Skip to content

Commit

Permalink
Improve the Wafer Map web worker class to support offscreen rendering (
Browse files Browse the repository at this point in the history
…#1929)

# Pull Request

## 🤨 Rationale

Wafer Map's canvas is the main component used to draw the actual dies on
a wafer


![image](https://github.com/ni/nimble/assets/110180309/05141b98-2158-4909-ab3d-54781307c219)

The old wafer renders the canvas on the main thread, for the new one we
want to render the canvas inside the web worker

<!---
Provide some background and a description of your work.
What problem does this change solve?

Include links to issues, work items, or other discussions.
-->

## 👩‍💻 Implementation

The main changes were done in matrix-renderer.ts, worker-renderer.ts and
index.ts
matrix-renderer.ts - holds the logic on how the canvas drawn, most of
the changes are straightforward besides the typed array parsing
algorithm from drawWafer(), but a comment explaining it can be found
there
worker-renderer.ts - holds the logic on how worker is setup
index.ts - creates the worker and transfers the canvas control from main
thread to the worker

less important changes were done in WaferMapTests.ts from the Blazor
component and zoom-handler.ts
WaferMapTests.ts - after adding a new wafer map tag inside the wafermap
template.ts the blazor tests were not able to distinguise between the 2
tags, I added IDs to each tag and changed the page.Locator to search for
the ID and not for the tag name
zoom-handler.ts - zoom behavior was managed inside update() method from
index.ts, as the constructor of ZoomHandler takes an wafer map as input
we implemented the observable design pattern using Notifier from Fast
Element, similar to how it is done for table nimble component


<!---
Describe how the change addresses the problem. Consider factors such as
complexity, alternative solutions, performance impact, etc.

Consider listing files with important changes or comment on them
directly in the pull request.
-->

## 🧪 Testing

Added a few unit tests and a chromatic test

<!---
Detail the testing done to ensure this submission meets requirements. 

Include automated/manual test additions or modifications, testing done
on a local build, private CI run results, and additional testing not
covered by automatic pull request validation. If any functionality is
not covered by automated testing, provide justification.
-->

## ✅ Checklist

<!--- Review the list and put an x in the boxes that apply or ~~strike
through~~ around items that don't (along with an explanation). -->

- [ ] I have updated the project documentation to reflect my changes or
determined no changes are needed.

---------

Co-authored-by: Natan Muntean <natan.muntean@ni.com>
Co-authored-by: Milan Raj <rajsite@users.noreply.github.com>
  • Loading branch information
3 people authored Apr 26, 2024
1 parent 0389e48 commit a3865cb
Show file tree
Hide file tree
Showing 23 changed files with 596 additions and 88 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "none",
"comment": "Added an additional canvas in wafer map template caused the blazor tests to fail due to missing unique identifiers for canvases, adding different ids for the canvases and explicitly telling the tests to look for one canvas id solved the issue",
"packageName": "@ni/nimble-blazor",
"email": "110180309+Razvan1928@users.noreply.github.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Updated wafer map web worker class to support offscreen rendering",
"packageName": "@ni/nimble-components",
"email": "110180309+Razvan1928@users.noreply.github.com",
"dependentChangeType": "patch"
}
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public async Task WaferMap_WithDiesAndColorScale_RendersColorsAsync()
{
await using var pageWrapper = await NewPageForRouteAsync("WaferMapRenderTest");
var page = pageWrapper.Page;
var canvas = page.Locator("canvas");
var canvas = page.Locator(".main-wafer");

await Assertions.Expect(canvas).ToBeVisibleAsync();
await Task.Delay(RenderingTimeout);
Expand All @@ -30,7 +30,7 @@ public async Task WaferMap_WithGridDimensions_IsValidAsync()
{
await using var pageWrapper = await NewPageForRouteAsync("WaferMapRenderTest");
var page = pageWrapper.Page;
var canvas = page.Locator("canvas");
var canvas = page.Locator(".main-wafer");
var validButton = page.Locator("nimble-button");
var textField = page.Locator("nimble-text-field");

Expand All @@ -46,7 +46,7 @@ public async Task WaferMap_WithHoverEvent_TriggersDieChangeEventAsync()
{
await using var pageWrapper = await NewPageForRouteAsync("WaferMapRenderTest");
var page = pageWrapper.Page;
var canvas = page.Locator("canvas");
var canvas = page.Locator(".main-wafer");
var textField = page.Locator("nimble-text-field");

await Assertions.Expect(canvas).ToBeVisibleAsync();
Expand Down
5 changes: 5 additions & 0 deletions packages/nimble-components/.prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
!*.mdx
!*.json
!src/
!build/
!/.storybook/

/src/**/*.*
Expand All @@ -19,6 +20,10 @@
/.storybook/*.*
!/.storybook/*.js

/build/**/*.*
!build/generate-workers/**/*.ts
!build/generate-workers/**/*.json

# Explicitly exclude the below files and folders
dist
node_modules
Expand Down
7 changes: 5 additions & 2 deletions packages/nimble-components/build/generate-workers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ const fileContent: string = `// eslint-disable-next-line no-template-curly-in-st
export const workerCode = ${JSON.stringify(sourceCode)};
`;

const renderFilePath: string = path.resolve(workersDirectory, 'matrix-renderer.ts');
const renderFilePath: string = path.resolve(
workersDirectory,
'matrix-renderer.ts'
);

writeFile(renderFilePath, fileContent);
writeFile(renderFilePath, fileContent);
Original file line number Diff line number Diff line change
@@ -1,23 +1,224 @@
import { expose } from 'comlink';
import type {
Dimensions,
Transform,
WaferMapMatrix,
WaferMapTypedMatrix
} from './types';

/**
* MatrixRenderer class is meant to be used within a Web Worker context,
* using Comlink to facilitate communication between the main thread and the worker.
* The MatrixRenderer class manages a matrix of dies, once an instance of MatrixRenderer is created,
* MatrixRenderer class is meant to be used within a Web Worker context,
* using Comlink to facilitate communication between the main thread and the worker.
* The MatrixRenderer class manages a matrix of dies, once an instance of MatrixRenderer is created,
* it is exposed to the main thread using Comlink's `expose` method.
* This setup is used in the wafer-map component to perform heavy computational duties
*/
export class MatrixRenderer {
public dieMatrix: Uint8Array = Uint8Array.from([]);
public columnIndexes = Int32Array.from([]);
public rowIndexes = Int32Array.from([]);
public values = Float64Array.from([]);
public scaledColumnIndex = Float64Array.from([]);
public scaledRowIndex = Float64Array.from([]);
public columnIndexPositions = Int32Array.from([]);
public canvas!: OffscreenCanvas;
public context!: OffscreenCanvasRenderingContext2D;
private scaleX: number = 1;
private scaleY: number = 1;
private baseX: number = 1;
private baseY: number = 1;
private dieDimensions: Dimensions = { width: 1, height: 1 };
private transform: Transform = { k: 1, x: 0, y: 0 };
private topLeftCanvasCorner!: { x: number; y: number };
private bottomRightCanvasCorner!: { x: number; y: number };
private readonly smallestMarginPossible: number = 20;
private margin: {
top: number;
right: number;
bottom: number;
left: number;
} = {
top: this.smallestMarginPossible,
right: this.smallestMarginPossible,
bottom: this.smallestMarginPossible,
left: this.smallestMarginPossible
};

public emptyMatrix(): void {
this.dieMatrix = Uint8Array.from([]);;
public calculateXScaledIndex(columnIndex: number): number {
return this.scaleX * columnIndex + this.baseX + this.margin.left;
}

public calculateYScaledIndex(rowIndex: number): number {
return this.scaleY * rowIndex + this.baseY + this.margin.top;
}

public setColumnIndexes(columnIndexes: Int32Array): void {
this.columnIndexes = columnIndexes;
if (columnIndexes.length === 0 || this.columnIndexes[0] === undefined) {
return;
}
const scaledColumnIndex = [
this.calculateXScaledIndex(this.columnIndexes[0])
];
const columnPositions = [0];
let prev = this.columnIndexes[0];
for (let i = 1; i < this.columnIndexes.length; i++) {
const xIndex = this.columnIndexes[i];
if (xIndex && xIndex !== prev) {
const scaledX = this.calculateXScaledIndex(
this.columnIndexes[i]!
);
scaledColumnIndex.push(scaledX);
columnPositions.push(i);
prev = xIndex;
}
}
this.scaledColumnIndex = Float64Array.from(scaledColumnIndex);
this.columnIndexPositions = Int32Array.from(columnPositions);
}

public updateMatrix(
data: Iterable<number>
public setRowIndexes(rowIndexesBuffer: Int32Array): void {
this.rowIndexes = rowIndexesBuffer;
this.scaledRowIndex = new Float64Array(this.rowIndexes.length);
for (let i = 0; i < this.rowIndexes.length; i++) {
this.scaledRowIndex[i] = this.calculateYScaledIndex(
this.rowIndexes[i]!
);
}
}

public setMargin(margin: {
top: number;
right: number;
bottom: number;
left: number;
}): void {
this.margin = margin;
}

public setCanvasCorners(
topLeft: { x: number; y: number },
bottomRight: { x: number; y: number }
): void {
this.dieMatrix = Uint8Array.from(data);
this.topLeftCanvasCorner = topLeft;
this.bottomRightCanvasCorner = bottomRight;
}

public setDiesDimensions(data: Dimensions): void {
this.dieDimensions = { width: data.width, height: data.height };
}

public setScaling(scaleX: number, scaleY: number): void {
this.scaleX = scaleX;
this.scaleY = scaleY;
}

public setBases(baseX: number, baseY: number): void {
this.baseX = baseX;
this.baseY = baseY;
}

public setTransform(transform: Transform): void {
this.transform = transform;
}

public setCanvas(canvas: OffscreenCanvas): void {
this.canvas = canvas;
this.context = canvas.getContext('2d')!;
}

public getMatrix(): WaferMapTypedMatrix {
return {
columnIndexes: this.columnIndexes,
rowIndexes: this.rowIndexes,
values: this.values
};
}

public emptyMatrix(): void {
this.columnIndexes = Int32Array.from([]);
this.rowIndexes = Int32Array.from([]);
this.values = Float64Array.from([]);
}

public scaleCanvas(): void {
this.context.translate(this.transform.x, this.transform.y);
this.context.scale(this.transform.k, this.transform.k);
}

public updateMatrix(data: WaferMapMatrix): void {
this.columnIndexes = Int32Array.from(data.columnIndexes);
this.rowIndexes = Int32Array.from(data.rowIndexes);
this.values = Float64Array.from(data.values);
}

public setCanvasDimensions(data: Dimensions): void {
this.canvas.width = data.width;
this.canvas.height = data.height;
}

public getCanvasDimensions(): Dimensions {
return {
width: this.canvas.width,
height: this.canvas.height
};
}

public clearCanvas(): void {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
}

public drawWafer(): void {
this.context.restore();
this.context.save();
this.clearCanvas();
this.scaleCanvas();
if (
this.topLeftCanvasCorner === undefined
|| this.bottomRightCanvasCorner === undefined
) {
throw new Error('Canvas corners are not set');
}
for (let i = 0; i < this.scaledColumnIndex.length; i++) {
const scaledX = this.scaledColumnIndex[i]!;
if (
!(
scaledX >= this.topLeftCanvasCorner.x
&& scaledX < this.bottomRightCanvasCorner.x
)
) {
continue;
}

// columnIndexPositions is used to get chunks to determine the start and end index of the column, it looks something like [0, 1, 4, 9, 12]
// This means that the first column has a start index of 0 and an end index of 1, the second column has a start index of 1 and an end index of 4, and so on
// scaledRowIndex is used when we reach the end of the columnIndexPositions, when columnIndexPositions is [0, 1, 4, 9, 12], scaledRowIndex is 13
const columnEndIndex = this.columnIndexPositions[i + 1] !== undefined
? this.columnIndexPositions[i + 1]!
: this.scaledRowIndex.length;
for (
let columnStartIndex = this.columnIndexPositions[i]!;
columnStartIndex < columnEndIndex;
columnStartIndex++
) {
const scaledY = this.scaledRowIndex[columnStartIndex]!;
if (
!(
scaledY >= this.topLeftCanvasCorner.y
&& scaledY < this.bottomRightCanvasCorner.y
)
) {
continue;
}
// Fill style is temporary green for all dies, will be replaced with a color based on the value of the die in a future implementation
this.context.fillStyle = 'Green';
this.context.fillRect(
scaledX,
scaledY,
this.dieDimensions.width,
this.dieDimensions.height
);
}
}
}
}
expose(MatrixRenderer);
expose(MatrixRenderer);
Loading

0 comments on commit a3865cb

Please sign in to comment.