Skip to content

Conversation

@mateuszJS
Copy link
Owner

@mateuszJS mateuszJS commented May 11, 2025

Summary by CodeRabbit

  • New Features

    • Added GPU-based picking for interactive asset selection and hover effects.
    • Introduced visible borders around hovered assets.
    • Enabled explicit asset identification and management for images and points.
    • Added pointer tracking for improved mouse interactions.
  • Refactor

    • Simplified and unified asset handling using explicit IDs and hash maps.
    • Updated rendering pipeline to support per-vertex color and picking.
    • Streamlined shader and rendering logic for improved maintainability.
    • Enhanced vertex data construction and retrieval methods.
  • Bug Fixes

    • Improved reliability of asset selection and hover feedback.
  • Chores

    • Removed obsolete example tests and cleaned up configuration files.

@coderabbitai
Copy link

coderabbitai bot commented May 11, 2025

Walkthrough

This update refactors the Rust backend to store assets in a HashMap keyed by asset IDs, adds hover tracking and border vertex generation, and introduces geometric and color types along with line vertex computation. The WebGPU frontend gains pointer tracking, a GPU picking pipeline with asynchronous readback, and updated rendering to handle explicit asset IDs and borders. Tests and configs are adjusted accordingly.

Changes

File(s) Change Summary
crate/src/lib.rs, crate/src/texture.rs, crate/src/line.rs, crate/src/types.rs Refactored asset management to use HashMap<usize, Texture> with hover support; added methods for shader inputs, picking, borders; introduced Line struct for line vertex data; defined geometric types and color alias.
integration-tests/index.ts, integration-tests/tests/creator.spec.ts Updated tests to use explicit asset IDs in API calls; improved test flow with network idle wait and structured validation steps.
jest.config.ts, webpack.config.js Removed commented config lines from Jest; always include HtmlWebpackPlugin in Webpack plugins.
src/WebGPU/pick.ts, src/WebGPU/pointer.ts Added PickManager class for GPU picking with offscreen 1x1 render target and async readback; added pointer tracking and mouse event handlers for canvas.
src/WebGPU/programs/drawTriangle/getProgram.ts, src/WebGPU/programs/drawTriangle/shader.wgsl Updated triangle drawing pipeline to use vertex colors and a 4x4 matrix uniform; simplified shaders and rendering logic.
src/WebGPU/programs/pickTexture/getProgram.ts, src/WebGPU/programs/pickTexture/shader.wgsl Added new GPU pipeline and WGSL shaders for texture picking outputting object IDs as uints.
src/WebGPU/programs/initPrograms.ts Registered new pickTexture program in GPU program initialization.
src/WebGPU/programs/drawTexture/shader.wgsl Simplified fragment shader by removing an intermediate variable.
src/index.ts, src/run.ts Updated API to require explicit asset IDs for adding/updating images; initialized pointer handling; rendering loop draws assets and borders by ID; integrated GPU picking pass.
tests-examples/demo-todo-app.spec.ts Deleted the comprehensive Playwright test suite for the demo TodoMVC application.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Canvas
    participant Pointer
    participant PickManager
    participant State
    participant GPU

    User->>Canvas: Moves mouse
    Canvas->>Pointer: Update pointer.x/y
    User->>Canvas: Interacts (hover, click)
    Canvas->>PickManager: Request pick at pointer.x/y
    PickManager->>GPU: Render 1x1 pick texture with asset IDs
    GPU-->>PickManager: Write picked ID to buffer
    PickManager->>State: update_hover(picked_id)
    State-->>PickManager: hovered_asset_id updated
    PickManager-->>Canvas: Optionally trigger border redraw
Loading
sequenceDiagram
    participant App
    participant State
    participant Renderer
    participant GPU

    App->>State: add_texture(id, points, texture_id)
    App->>Renderer: runCreator(state, assetsList)
    loop For each asset ID in assetsList
        Renderer->>State: get_shader_input(asset_id)
        State-->>Renderer: ShaderInput (vertex data, texture id)
        Renderer->>GPU: drawTexture(vertex data, texture)
    end
    Renderer->>State: get_border()
    State-->>Renderer: Border vertex data (if hovered)
    Renderer->>GPU: drawTriangle(border vertex data)
Loading

Poem

A hop and a skip, I bounce with glee,
Now assets have IDs—so easy to see!
My pointer twitches, the GPU picks,
Hovered borders appear—what a neat trick!
With triangles and textures, the code’s quite sublime,
This bunny approves—one hop at a time!
🐇✨

Tip

⚡️ Faster reviews with caching
  • CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure Review - Disable Cache at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the Data Retention setting under your Organization Settings.

Enjoy the performance boost—your workflow just got faster.

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🔭 Outside diff range comments (1)
src/index.ts (1)

73-76: 🛠️ Refactor suggestion

Missing cleanup for mouse event listeners

The destroy method doesn't remove the event listeners added by initMouseController, which could lead to memory leaks if the creator is destroyed and recreated.

    destroy: () => {
      context.unconfigure()
      device.destroy()
+     // Clean up event listeners
+     canvas.removeEventListener('mouseleave', () => {})
+     canvas.removeEventListener('mousemove', () => {})
+     canvas.removeEventListener('mousedown', () => {})
+     canvas.removeEventListener('mouseup', () => {})
+     canvas.removeEventListener('wheel', () => {})
    }

For proper cleanup, consider refactoring initMouseController to return a cleanup function that removes all event listeners, or store the event handler functions to remove them specifically.

🧹 Nitpick comments (21)
src/run.ts (2)

31-34: Avoid per-frame allocations of Float32Array

Wrapping vertex_data in new Float32Array() every frame triggers GC pressure. Cache typed arrays once:

-const { texture_id, vertex_data } = state.get_shader_input(id)
-drawTexture(pass, matrix, new Float32Array(vertex_data), textures[texture_id])
+const { texture_id, vertex_data_buffer } = state.get_shader_input(id) // expose a cached buffer
+drawTexture(pass, matrix, vertex_data_buffer, textures[texture_id])

If exposing a buffer is not feasible, reuse a scratch buffer to reduce allocations.


53-53: asyncPick not awaited – race conditions possible

pickManager.asyncPick(state) returns a promise (per its name). If a rejection occurs, it will be swallowed by the event loop. Consider either:

pickManager.asyncPick(state).catch(console.error)

or converting the outer draw to async and await the call to surface errors during development.

src/WebGPU/programs/initPrograms.ts (1)

8-18: Missing re-export from barrel file breaks tree-shaking

You added pickTexture to the runtime but forgot to re-export it from any public barrel (if one exists). Double-check modules such as src/WebGPU/programs/index.ts or higher-level aggregators to ensure consumers can import { pickTexture } from .../programs'.

If no barrel exists, ignore this note.

src/WebGPU/pointer.ts (4)

7-8: Empty mouseleave handler

The mouseleave handler is empty. Consider updating the pointer state or adding a comment explaining the intended future functionality.

  canvas.addEventListener('mouseleave', () => {
+   // Reset pointer or set a flag indicating mouse is outside canvas
  })

16-18: Unused parameter in mousedown event handler

The 'e' parameter is defined but never used in the mousedown handler.

-  canvas.addEventListener('mousedown', e => {
+  canvas.addEventListener('mousedown', () => {

  })
🧰 Tools
🪛 ESLint

[error] 16-16: 'e' is defined but never used.

(@typescript-eslint/no-unused-vars)


20-22: Unused parameter in mouseup event handler

The 'e' parameter is defined but never used in the mouseup handler.

-  canvas.addEventListener('mouseup', e => {
+  canvas.addEventListener('mouseup', () => {

  })
🧰 Tools
🪛 ESLint

[error] 20-20: 'e' is defined but never used.

(@typescript-eslint/no-unused-vars)


24-26: Console log in wheel event handler

The wheel event handler only logs to the console. Consider implementing meaningful functionality or adding a comment explaining this is for development/debugging purposes.

  canvas.addEventListener("wheel", (event) => {
-   console.log(event.deltaY)
+   // TODO: Implement zoom functionality using event.deltaY
+   console.log("Wheel delta:", event.deltaY)
  })
crate/src/line.rs (2)

5-11: Function signature looks good, but parameter name is inconsistent

The parameter name points_a (plural) doesn't match with its usage as a single point.

    pub fn get_vertex_data<P: HasCoords>(
-       points_a: &P,
+       point_a: &P,
        point_b: &P,
        width: f32,
        color: Color,
    ) -> Vec<f32> {

20-45: Vertex generation for the line rectangle

The vertex data creates two triangles to form a rectangular line. This is correct for rendering with triangles, but you might consider using a different winding order for better readability.

        let vertex_data: [Point; 6] = [
            Point {
                x: ax - half_width * angle.cos(),
                y: ay - half_width * angle.sin(),
            },
            Point {
                x: ax + half_width * angle.cos(),
                y: ay + half_width * angle.sin(),
            },
            Point {
                x: bx + half_width * angle.cos(),
                y: by + half_width * angle.sin(),
            },
-           Point {
-               x: bx - half_width * angle.cos(),
-               y: by - half_width * angle.sin(),
-           },
-           Point {
-               x: ax - half_width * angle.cos(),
-               y: ay - half_width * angle.sin(),
-           },
-           Point {
-               x: bx + half_width * angle.cos(),
-               y: by + half_width * angle.sin(),
-           },
+           // Second triangle (completing the rectangle)
+           Point {
+               x: bx + half_width * angle.cos(),
+               y: by + half_width * angle.sin(),
+           },
+           Point {
+               x: bx - half_width * angle.cos(),
+               y: by - half_width * angle.sin(),
+           },
+           Point {
+               x: ax - half_width * angle.cos(),
+               y: ay - half_width * angle.sin(),
+           },
        ];
crate/src/types.rs (1)

24-39: VertexPoint struct and implementation look good

The VertexPoint struct extends the basic Point with texture coordinates u and v, which is useful for texture mapping. The HasCoords implementation is appropriate.

Since both Point and VertexPoint have identical HasCoords implementations, you might consider a derive macro or a blanket implementation for any struct with x and y fields to reduce code duplication.

// Example of a blanket implementation (not using a macro)
impl<T> HasCoords for T 
where 
    T: AsRef<Self>,
    Self: {x: f32, y: f32}
{
    fn x(&self) -> f32 { self.x }
    fn y(&self) -> f32 { self.y }
}
crate/src/texture.rs (2)

18-31: Consider using a constant for the triangle definition.

The refactored get_vertex_data method looks good, but the hardcoded vertex indices array is duplicated in get_vertex_pick_data. Consider extracting this into a constant to avoid duplication.

 impl Texture {
+    const TRIANGLE_INDICES: [usize; 6] = [0, 1, 2, 2, 3, 0];
     
     pub fn get_vertex_data(&self) -> Vec<f32> {
-        let points: [&VertexPoint; 6] = [
-            &self.points[0],
-            &self.points[1],
-            &self.points[2],
-            &self.points[2],
-            &self.points[3],
-            &self.points[0],
-        ];
+        let points: Vec<&VertexPoint> = Self::TRIANGLE_INDICES.iter()
+            .map(|&idx| &self.points[idx])
+            .collect();
         points
             .iter()
             .flat_map(|point| vec![point.x, point.y, 0.0, 1.0, point.u, point.v])
             .collect()
     }

43-56: Refactor to reuse vertex index pattern.

This method duplicates vertex index pattern from get_vertex_data. If you implement the above refactoring suggestion, you should also update this method accordingly.

     pub fn get_vertex_pick_data(&self) -> Vec<f32> {
-        let points: [&VertexPoint; 6] = [
-            &self.points[0],
-            &self.points[1],
-            &self.points[2],
-            &self.points[2],
-            &self.points[3],
-            &self.points[0],
-        ];
+        let points: Vec<&VertexPoint> = Self::TRIANGLE_INDICES.iter()
+            .map(|&idx| &self.points[idx])
+            .collect();
         points
             .iter()
             .flat_map(|point| vec![point.x, point.y, 0.0, 1.0, point.u, point.v, self.id])
             .collect()
     }
src/WebGPU/programs/pickTexture/shader.wgsl (2)

21-29: Remove commented code when it's no longer needed.

There's a commented note about passing offsets instead of positions. If this optimization is planned for the future, consider adding a TODO comment for clarity, otherwise remove it.

  var out: VertexOutput;
-  // maybe we should pass offsets from the position instead of... position?
  out.position = u.worldViewProjection * vert.position;

32-41: Remove commented function signature.

The commented-out function signature on line 32 should be removed.

-// @fragment fn fs(in: VertexOutput) -> @location(0) vec4f {
 @fragment fn fs(in: VertexOutput) -> @location(0) u32 {

The fragment shader logic for discarding transparent pixels and returning the ID as a u32 looks correct.

src/WebGPU/programs/pickTexture/getProgram.ts (4)

26-33: Clean up commented code and clarify attribute formats.

There's an unused commented attribute on line 32. Additionally, the vertex attribute format comments could be more descriptive.

           attributes: [
-            {shaderLocation: 0, offset: 0, format: 'float32x4'},  // destination position
-            {shaderLocation: 1, offset: 16, format: 'float32x2'},  // source position
-            {shaderLocation: 2, offset: 16 + 8, format: 'float32'},  // id
-            // {shaderLocation: 3, offset: 16 + 8, format: 'float32x3'},  // id
+            {shaderLocation: 0, offset: 0, format: 'float32x4'},       // position (x, y, z, w)
+            {shaderLocation: 1, offset: 16, format: 'float32x2'},      // texture coordinates (u, v)
+            {shaderLocation: 2, offset: 16 + 8, format: 'float32'},    // object id
           ] as const,

40-43: Remove commented-out format line.

Since you're explicitly using the 'r32uint' format for picking, the commented-out line referencing debugPresentationFormat can be removed.

       targets: [{
-        // format: debugPresentationFormat,
         format: 'r32uint',
       }],

45-53: Consider whether primitive and depth-stencil settings are needed.

There are commented out primitive cullMode and depthStencil settings. If these settings are not relevant for picking, consider removing the commented code. If they might be needed later, add a TODO comment explaining when they should be uncommented.

-    // primitive: {
-    //   cullMode: 'back',
-    // },
-    // depthStencil: {
-    //   depthWriteEnabled: true,
-    //   depthCompare: 'less',
-    //   format: 'depth24plus',
-    // },

98-98: Remove commented-out code.

There's a commented-out matrix translation operation. Remove it if it's not needed.

-    // const translateWorldProjMatrix = mat4.translate(worldProjectionMatrix, [])
src/WebGPU/pick.ts (1)

56-61: Dead code & linter noise – remove or put to use

pickDepthTexture and the depthStencilAttachment block are allocated/commented but never used.
Likewise, the debugRenderPass argument is unused and triggers the ESLint error you’ve seen.

Cleaning these up will remove needless GPU memory consumption and silence the linter:

-  private pickDepthTexture: GPUTexture-    this.pickDepthTexture = device.createTexture({
-      size: [1, 1],
-      format: 'depth24plus',
-      usage: GPUTextureUsage.RENDER_ATTACHMENT,
-    })

Or – if you plan to use a depth attachment later – keep it but reference it in descriptor.depthStencilAttachment.

Also applies to: 29-33

src/WebGPU/programs/drawTriangle/getProgram.ts (1)

76-82: Performance: vertex buffer & bind group are recreated every draw

Creating GPU buffers and bind groups inside the hot draw loop is expensive.
Consider:

  1. Keep a cache keyed by vertexData.byteLength.
  2. Re-use a single bind group – it only contains the uniform buffer.

This will reduce stutter when many borders are drawn on hover.

Also applies to: 84-92

crate/src/lib.rs (1)

104-128: get_border allocates every frame – cache or stream?

Rebuilding the full border vertex vector on each call can become expensive when borders are thick (width = 20.0) and assets have many points.
If the hover state does not change every frame, cache the generated buffer and only invalidate it in update_hover.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9b5f081 and edcf703.

⛔ Files ignored due to path filters (1)
  • integration-tests/tests/creator.spec.ts-snapshots/after-hover-chromium.png is excluded by !**/*.png
📒 Files selected for processing (19)
  • crate/src/lib.rs (1 hunks)
  • crate/src/line.rs (1 hunks)
  • crate/src/texture.rs (3 hunks)
  • crate/src/types.rs (1 hunks)
  • integration-tests/index.ts (2 hunks)
  • integration-tests/tests/creator.spec.ts (1 hunks)
  • jest.config.ts (1 hunks)
  • src/WebGPU/pick.ts (1 hunks)
  • src/WebGPU/pointer.ts (1 hunks)
  • src/WebGPU/programs/drawTexture/shader.wgsl (1 hunks)
  • src/WebGPU/programs/drawTriangle/getProgram.ts (1 hunks)
  • src/WebGPU/programs/drawTriangle/shader.wgsl (1 hunks)
  • src/WebGPU/programs/initPrograms.ts (3 hunks)
  • src/WebGPU/programs/pickTexture/getProgram.ts (1 hunks)
  • src/WebGPU/programs/pickTexture/shader.wgsl (1 hunks)
  • src/index.ts (3 hunks)
  • src/run.ts (2 hunks)
  • tests-examples/demo-todo-app.spec.ts (0 hunks)
  • webpack.config.js (1 hunks)
💤 Files with no reviewable changes (1)
  • tests-examples/demo-todo-app.spec.ts
🧰 Additional context used
🧬 Code Graph Analysis (6)
src/index.ts (4)
src/types.ts (1)
  • Point (1-4)
src/WebGPU/pointer.ts (1)
  • initMouseController (3-27)
src/run.ts (1)
  • runCreator (10-59)
src/WebGPU/getTexture/index.ts (1)
  • createTextureFromSource (106-117)
crate/src/texture.rs (3)
src/types.ts (1)
  • Point (1-4)
crate/src/line.rs (1)
  • get_vertex_data (6-55)
crate/src/types.rs (6)
  • x (5-5)
  • x (16-18)
  • x (33-35)
  • y (6-6)
  • y (19-21)
  • y (36-38)
src/WebGPU/pick.ts (1)
src/WebGPU/pointer.ts (1)
  • pointer (1-1)
crate/src/types.rs (1)
src/types.ts (1)
  • Point (1-4)
crate/src/lib.rs (3)
src/types.ts (2)
  • Line (6-6)
  • Point (1-4)
crate/src/texture.rs (2)
  • new (10-16)
  • get_vertex_data (18-31)
crate/src/line.rs (1)
  • get_vertex_data (6-55)
src/WebGPU/programs/drawTriangle/getProgram.ts (4)
src/WebGPU/programs/draw3dModel/getProgram.ts (1)
  • getProgram (3-97)
src/WebGPU/programs/draw3dModelLight/getProgram.ts (1)
  • getProgram (10-123)
src/WebGPU/programs/drawTexture/getProgram.ts (1)
  • getProgram (6-114)
src/WebGPU/programs/initPrograms.ts (1)
  • drawTriangle (10-10)
🪛 ESLint
src/WebGPU/pointer.ts

[error] 16-16: 'e' is defined but never used.

(@typescript-eslint/no-unused-vars)


[error] 20-20: 'e' is defined but never used.

(@typescript-eslint/no-unused-vars)

src/WebGPU/programs/pickTexture/getProgram.ts

[error] 7-7: 'debugPresentationFormat' is defined but never used.

(@typescript-eslint/no-unused-vars)

src/WebGPU/pick.ts

[error] 40-40: 'debugRenderPass' is defined but never used.

(@typescript-eslint/no-unused-vars)

🔇 Additional comments (34)
jest.config.ts (1)

15-15: Style cleanup: Removed unnecessary semicolons

The trailing semicolons have been removed, which is consistent with modern TypeScript style. This change is purely aesthetic and has no functional impact.

Also applies to: 17-17

webpack.config.js (1)

100-105: HtmlWebpackPlugin now included unconditionally

The HtmlWebpackPlugin is now included in the test configuration regardless of the environment, ensuring HTML template generation for integration tests in both development and production modes. This change supports the new asset picking functionality being tested.

src/WebGPU/programs/drawTexture/shader.wgsl (1)

29-29: Code simplification in fragment shader

Removed the intermediate texel variable and directly returned the result of textureSample(). This is a good simplification that maintains the same functionality while reducing code verbosity.

integration-tests/index.ts (3)

4-5: Added explicit asset identification

The introduction of a constant ASSET_ID aligns with the new asset management system that uses explicit IDs instead of implicit ordering. This is a good practice for clarity and maintainability.


18-18: Updated addImage API to use explicit asset ID

The addImage method now accepts an explicit asset ID as its first parameter, matching the updated API that manages assets by ID rather than by position in a collection.


25-30: Updated updatePoints API to use explicit asset ID

The updatePoints method now accepts an explicit asset ID as its first parameter, consistent with the new asset management approach. This allows for more explicit targeting of specific assets when updating their coordinates.

integration-tests/tests/creator.spec.ts (1)

24-24: Possible hang: networkidle may never be reached

await page.waitForLoadState('networkidle') is great for traditional sites, but if your app opens long-lived connections (e.g. WebSocket, EventSource, streaming fetch) the network will never go “idle”, causing the test to stall indefinitely.
Consider one of the following:

-await page.waitForLoadState('networkidle')
+// Wait for main resources only – avoids hanging on long-poll / WS
+await page.waitForLoadState('domcontentloaded')
+// or: await page.waitForTimeout(500);  // tiny static delay

Verify that no flaky timeouts appear in CI before merging.

src/run.ts (1)

43-48: pass is ended before being reused

pass.end() finalises the render pass; passing the ended pass object to pickManager.render is undefined behaviour – the spec forbids using a pass after end().

If pickManager.render only needs the colour attachment texture, pass the texture view instead, or restructure:

// 1️⃣ main colour pass
const colourPass = encoder.beginRenderPass(descriptor)
// draw…
colourPass.end()

// 2️⃣ pick pass (separate)
pickManager.render(encoder, matrix, /* draw fn */)

Please verify that no validation errors appear in the browser console.

src/WebGPU/programs/initPrograms.ts (1)

30-31: Order of initialisation

If any of the shader programs depend on pickTexture’s existence (e.g. sharing bind-group layouts) and assume the variables are initialised top-to-bottom, be aware that TypeScript hoists let declarations but not initialisations. Your current order is fine, just make sure no earlier program uses pickTexture during its own factory call.

src/WebGPU/pointer.ts (3)

1-2: Export of mutable pointer object looks good

The exported pointer object with x and y coordinates provides a clean way for other modules to access mouse position data.


3-6: Initialization looks good

Good practice to reset the pointer coordinates when initializing the controller.


10-14: Mouse movement tracking looks good

Correctly calculates pointer coordinates relative to the canvas position.

crate/src/line.rs (3)

1-4: Good use of the HasCoords trait

Nice import of the HasCoords trait to make the Line implementation work with different point types.


12-19: Issue with line cap calculations

The calculation of the line extension points (ax, ay, bx, by) uses the parallel angle's cos/sin directly but is also subtracting/adding half_width. This might result in incorrect line caps, especially for short lines.

        let half_width = width / 2.0;
        let parallel_angle = (point_b.y() - points_a.y()).atan2(point_b.x() - points_a.x());
        let angle = parallel_angle + std::f32::consts::PI / 2.0; // perpendicular angle
-       let ax = points_a.x() - half_width * parallel_angle.cos();
-       let ay = points_a.y() - half_width * parallel_angle.sin();
-       let bx = point_b.x() + half_width * parallel_angle.cos();
-       let by = point_b.y() + half_width * parallel_angle.sin();
+       // No need to extend start/end points for a simple line with thickness
+       let ax = points_a.x();
+       let ay = points_a.y();
+       let bx = point_b.x();
+       let by = point_b.y();

Please verify that the line caps render correctly, especially for diagonal lines. Consider using a more traditional approach where you don't modify start/end points for thickness.


47-55: Vertex data flattening looks good

The implementation correctly flattens the vertex array into a format suitable for GPU rendering, including position and color attributes.

crate/src/types.rs (3)

3-7: Good abstraction with HasCoords trait

The HasCoords trait provides a clean abstraction for accessing coordinates from different point types.


9-22: Point struct and implementation look good

The Point struct is properly defined with x and y fields and correctly implements the HasCoords trait. The Serialize/Deserialize derivation is appropriate for data that needs to be exchanged between Rust and JavaScript.


41-41: Color type definition is clear and concise

The Color type alias is a good way to represent RGBA values consistently throughout the codebase.

src/index.ts (6)

9-9: Import of pointer controller looks good

Importing the initMouseController function is necessary for the hover functionality.


11-15: CreatorAPI interface update with asset IDs

Good update to require explicit asset IDs in both addImage and updatePoints methods, which improves API clarity and aligns with the backend changes.


42-42: Initialization of mouse controller

Properly initializes the mouse controller with the canvas element.


45-46: Asset list tracking

Good addition of an assetsList array to track asset IDs, and passing it to runCreator for explicit asset management.


50-69: addImage implementation with ID support

The addImage method now correctly accepts an ID parameter and passes it to the state.add_texture method. The asset ID is also added to the assetsList for tracking.


70-72: updatePoints implementation with ID support

The updatePoints method now correctly uses the ID parameter to update points for a specific asset.

crate/src/texture.rs (2)

4-6: Update field visibility to match access requirements.

The id field is private but points and texture_id are public. Consider whether these visibility choices align with your encapsulation needs:

  • Making id private is good for encapsulation since it's only used internally for picking
  • points is public to support border drawing
  • texture_id is public for external texture identification

10-16: LGTM!

The constructor correctly initializes the Texture with the necessary fields, converting the usize ID to f32 for GPU compatibility.

src/WebGPU/programs/drawTriangle/shader.wgsl (4)

1-8: LGTM!

The Vertex and Uniforms struct definitions are well-defined with appropriate type annotations and locations for GPU attribute binding.


10-13: LGTM!

The VertexOutput structure correctly passes through the vertex data from the vertex shader to the fragment shader with appropriate location decorators.


17-23: LGTM!

The vertex shader function properly transforms the vertex position using the world view projection matrix and passes the color through to the fragment shader.


25-27: LGTM!

The fragment shader correctly outputs the interpolated color received from the vertex shader.

src/WebGPU/programs/pickTexture/shader.wgsl (3)

1-5: LGTM!

The Vertex struct is correctly defined with position, UV coordinates, and ID attributes at appropriate shader locations.


7-15: Good use of flat interpolation for the ID.

The @interpolate(flat) decorator on the ID field ensures the vertex ID is not interpolated between vertices, which is essential for accurate picking.


17-20: LGTM!

Proper binding group setup for the uniforms, sampler, and texture.

src/WebGPU/pick.ts (1)

63-73: Coordinate transform may be flipped on Y – double-check

ty is computed with 2 * (pointer.y / clientHeight) - 1, which maps y = 0 to -1 (bottom in NDC) and y = h to +1 (top).
Canvas pointer events usually have the origin at the top-left, meaning the mapping is inverted.
If you observe inverted picks on the Y-axis, invert the sign:

const ty = - (2 * (pointer.y / clientHeight) - 1)

Comment on lines +39 to +41
/** =========DISPLAYS BORDER AROUND HOVERED ASSET============ */
await page.mouse.move(200, 300)
await expect(canvas).toHaveScreenshot('after-hover.png')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Hard-coded mouse coordinates make the test brittle

page.mouse.move(200, 300) assumes the asset to hover is always at (200, 300). Any layout change will break the test.

Recommended:

const bbox = await canvas.boundingBox()
// move to the center of the canvas (or compute the asset position)
await page.mouse.move(bbox.x + bbox.width / 2, bbox.y + bbox.height / 2)

This uses DOM geometry instead of magic numbers and will survive responsive layout tweaks.

Comment on lines +36 to 39
const borderVertexData = state.get_border()
if (borderVertexData.length > 0) {
drawTriangle(pass, matrix, borderVertexData)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

drawTriangle expects Float32Array – currently passing plain array

state.get_border() seems to return a JS array; drawTriangle (like drawTexture) probably expects a typed buffer. Pass a Float32Array to prevent undefined behaviour on some drivers:

-const borderVertexData = state.get_border()
+const borderVertexData = new Float32Array(state.get_border())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const borderVertexData = state.get_border()
if (borderVertexData.length > 0) {
drawTriangle(pass, matrix, borderVertexData)
}
const borderVertexData = new Float32Array(state.get_border())
if (borderVertexData.length > 0) {
drawTriangle(pass, matrix, borderVertexData)
}

Comment on lines +17 to 18
assetsList: number[]
) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

assetsList captured only once – newly added assets never render

Because assetsList is passed by value, the array is captured at runCreator invocation time. If the Rust backend adds or removes assets later, the render loop will not see them.

Consider querying the State each frame:

-assetsList.forEach((id) => {
+state.get_asset_ids().forEach((id) => {

and drop the assetsList parameter entirely (or keep it as an optimisation cache that you refresh when state.needsRefresh is set).

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +5 to +8
export default function getProgram(
device: GPUDevice,
debugPresentationFormat: GPUTextureFormat
) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Unused parameter detected.

The debugPresentationFormat parameter is defined but never used in the function.

 export default function getProgram(
   device: GPUDevice,
-  debugPresentationFormat: GPUTextureFormat
+  // Remove unused parameter or use it in the function
 ) {

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 ESLint

[error] 7-7: 'debugPresentationFormat' is defined but never used.

(@typescript-eslint/no-unused-vars)

Comment on lines 84 to 93

// bind group should be pre-created and reuse instead of constantly initialized
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: uniformBuffer }},
{ binding: 1, resource: sampler },
{ binding: 2, resource: texture.createView() },
],
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Implement bind group optimization note.

Line 85 notes that bind groups should be pre-created and reused, but this optimization isn't implemented. Consider moving the bind group creation outside the function and implementing a caching strategy.

+  // Create bind group layout once
+  const bindGroupLayout = pipeline.getBindGroupLayout(0)
+  
+  // Cache for texture view bind groups
+  const textureBindGroups = new Map<GPUTexture, GPUBindGroup>()
+
   return function drawTexture(
     pass: GPURenderPassEncoder,
     worldProjectionMatrix: Float32Array,
     vertexData: Float32Array<ArrayBufferLike>,
     texture: GPUTexture,
   ) {
     // ... vertex buffer creation code ...

-    // bind group should be pre-created and reuse instead of constantly initialized
-    const bindGroup = device.createBindGroup({
-      layout: pipeline.getBindGroupLayout(0),
-      entries: [
-        { binding: 0, resource: { buffer: uniformBuffer }},
-        { binding: 1, resource: sampler },
-        { binding: 2, resource: texture.createView() },
-      ],
-    })
+    // Get or create bind group for this texture
+    let bindGroup = textureBindGroups.get(texture)
+    if (!bindGroup) {
+      bindGroup = device.createBindGroup({
+        layout: bindGroupLayout,
+        entries: [
+          { binding: 0, resource: { buffer: uniformBuffer }},
+          { binding: 1, resource: sampler },
+          { binding: 2, resource: texture.createView() },
+        ],
+      })
+      textureBindGroups.set(texture, bindGroup)
+    }

🌐 Web query:

What's the best practice for caching WebGPU bind groups with dynamic texture resources?

💡 Result:

WebGPU bind groups are a crucial part of the WebGPU API that connect shader resources to actual GPU resources. When dealing with dynamic texture resources that change frequently, efficient caching becomes essential for performance.

Understanding Bind Groups and Their Limitations

Bind groups in WebGPU are immutable once created, meaning their resources cannot be changed after creation, though the contents of those resources can be updated[1]. This presents a challenge when working with dynamic resources like textures that may need to be swapped frequently.

Unlike buffers, which support dynamic offsets to change binding parameters without recreating bind groups, textures don't have an equivalent mechanism[3]. This means that when you need to swap texture resources, you typically must create new bind groups.

Efficient Caching Strategies

Bind Group Cache Implementation

One effective approach is to implement a cache for bind groups to avoid recreating them each frame. Babylon.js, for example, uses a node tree structure for their bind group cache:

class WebGPUBindGroupCacheNode {
  public values: { [id: number]: WebGPUBindGroupCacheNode };
  public bindGroups: GPUBindGroup[];
  
  constructor() {
    this.values = {};
  }
}

The cache uses resource IDs as keys, where the ID for uniform/storage buffers and textures is typically the uniqueId property of the corresponding class[2].

Optimization Techniques

  1. Context-Based Optimization: If draw and material contexts haven't changed since the last cache query, return existing bind groups without traversing the cache. This works because the draw context holds uniform/storage buffers and the material context contains textures and samplers[2].

  2. Linear Allocation: For highly dynamic resources, consider implementing a system similar to "dynamic bind groups" that allocate linearly and efficiently fill descriptors[3].

  3. Group Index Organization: Organize your bind groups by frequency of change. Place data that changes least frequently in @group(0) and each subsequent group should contain progressively more frequently changing data. While the API is technically agnostic to this order, underlying native APIs may have performance preferences[1].

Monitoring Cache Performance

To ensure your caching strategy is effective, monitor metrics such as:

  • Total number of bind groups created
  • Number of bind groups created per frame
  • Number of bind group lookups per frame[2]

For optimal performance, the number of bind groups created per frame should approach zero on average.

Handling Dynamic Textures

When working with frequently changing textures (like render targets or streaming content):

  1. Accept the overhead: Creating bind groups on the fly is not as expensive as you might think. The WebGPU implementation may use caching under the hood[7].

  2. Resource pooling: Implement a pool of pre-created textures and bind groups that can be reused.

  3. Framegraph approach: When implementing post-processing effects that create new textures every frame, implement caching policies to reduce texture/bind group creation[8].

  4. Consider ImageBitmap optimization: When loading textures from external sources, use ImageBitmap which decodes images off the main thread into a GPU-friendly format[6].

While there has been discussion about introducing a BindGroupArena concept to manage dynamic bind groups[7], the current best practice is to implement your own caching mechanism while understanding that the overhead of creating bind groups is designed to be relatively low in WebGPU.

By carefully organizing resources based on their update frequency and implementing an effective caching strategy, you can minimize the performance impact of working with dynamic texture resources in WebGPU.

Citations:


Optimize bind group creation by caching

The WebGPU best practices confirm that bind groups are immutable and don’t support dynamic texture binding via offsets—so creating a new bind group each draw is wasteful. Move layout creation and cache initialization outside the draw function, then look up or create per-texture bind groups:

• File: src/WebGPU/programs/pickTexture/getProgram.ts
– Above the returned drawTexture function, add:
```ts
// Create bind group layout once
const bindGroupLayout = pipeline.getBindGroupLayout(0);

// Cache bind groups per texture
const textureBindGroups = new Map<GPUTexture, GPUBindGroup>();
```

• Inside drawTexture, replace the on-the-fly creation with a lookup:

-   // bind group should be pre-created and reuse instead of constantly initialized
-   const bindGroup = device.createBindGroup({
-     layout: pipeline.getBindGroupLayout(0),
-     entries: [
-       { binding: 0, resource: { buffer: uniformBuffer } },
-       { binding: 1, resource: sampler },
-       { binding: 2, resource: texture.createView() },
-     ],
-   });
+   // Get or create bind group for this texture
+   let bindGroup = textureBindGroups.get(texture);
+   if (!bindGroup) {
+     bindGroup = device.createBindGroup({
+       layout: bindGroupLayout,
+       entries: [
+         { binding: 0, resource: { buffer: uniformBuffer } },
+         { binding: 1, resource: sampler },
+         { binding: 2, resource: texture.createView() },
+       ],
+     });
+     textureBindGroups.set(texture, bindGroup);
+   }

This ensures bind groups are only created once per unique texture, driving your per-frame creation count toward zero.

Comment on lines +18 to +34
this.pickBuffer = device.createBuffer({
size: NUM_PIXELS * 4,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
})

this.pickTexture = device.createTexture({
size: [1, 1],
format: 'r32uint',
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
})

this.pickDepthTexture = device.createTexture({
size: [1, 1],
format: 'depth24plus',
usage: GPUTextureUsage.RENDER_ATTACHMENT,
})
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Critical: bytesPerRow / buffer size are calculated incorrectly – will fail GPU validation

bytesPerRow has to be a multiple of 256 bytes but must not be multiplied by 256 again.
For NUM_PIXELS = 1 the current expression produces 66304 bytes, while the buffer you create is only 4 bytes long – any copy will trigger a validation error (“destination out of bounds”).

Fix: align to 256 bytes once and allocate the buffer for the aligned size:

-    this.pickBuffer = device.createBuffer({
-      size: NUM_PIXELS * 4,
+    // Each row must be 256-byte aligned (WebGPU spec).
+    const bytesPerRow = Math.ceil(NUM_PIXELS * 4 / 256) * 256
+    this.pickBuffer = device.createBuffer({
+      size: bytesPerRow,
       usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
     })
…
-    encoder.copyTextureToBuffer({
-      texture: this.pickTexture,
-      origin: { x: 0, y: 0 }
-    }, {
-      buffer: this.pickBuffer,
-      bytesPerRow: ((NUM_PIXELS * 4 + 255) | 0) * 256,
-      rowsPerImage: 1,
-    }, {
-      width: NUM_PIXELS,
-    })
+    encoder.copyTextureToBuffer(
+      { texture: this.pickTexture },
+      {
+        buffer: this.pickBuffer,
+        bytesPerRow,
+        rowsPerImage: 1,
+      },
+      { width: NUM_PIXELS, height: 1 }
+    )

Without this change, the picking pass will never work on any compliant implementation.

Also applies to: 87-97

Comment on lines +33 to 35
assets: HashMap<usize, Texture>,
hovered_asset_id: usize, // 0 -> no asset is hovered
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Sentinel value 0 for “no hover” collides with valid asset IDs

hovered_asset_id defaults to 0, but asset IDs are arbitrary usizes – 0 is a perfectly valid ID and will never be treated as “no hover”.

Use Option<usize> instead:

-    hovered_asset_id: usize, // 0 -> no asset is hovered
+    hovered_asset_id: Option<usize>,-        hovered_asset_id: 0,
+        hovered_asset_id: None,-    pub fn update_hover(&mut self, id: usize) {
-        self.hovered_asset_id = id
+    pub fn update_hover(&mut self, id: usize) {
+        self.hovered_asset_id = Some(id)
     }

get_border will then match on self.hovered_asset_id safely.

Also applies to: 100-104

Repository owner deleted a comment from coderabbitai bot May 11, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
src/WebGPU/programs/pickTexture/getProgram.ts (2)

5-8: ⚠️ Potential issue

Remove unused parameter.

The debugPresentationFormat parameter is defined but never used in the function. It appears to be related to the commented-out line 41 but is not currently needed.

export default function getProgram(
  device: GPUDevice,
-  debugPresentationFormat: GPUTextureFormat
) {
🧰 Tools
🪛 ESLint

[error] 7-7: 'debugPresentationFormat' is defined but never used.

(@typescript-eslint/no-unused-vars)


85-94: 🛠️ Refactor suggestion

Implement bind group caching for better performance.

Creating a new bind group on every render call is inefficient. As noted in the TODO comment, bind groups should be pre-created and reused. This is especially important for performance in WebGPU applications.

+  // Create bind group layout once
+  const bindGroupLayout = pipeline.getBindGroupLayout(0);
+
+  // Cache for texture view bind groups
+  const textureBindGroups = new Map<GPUTexture, GPUBindGroup>();
+
   return function drawTexture(
     pass: GPURenderPassEncoder,
     worldProjectionMatrix: Float32Array,
     vertexData: Float32Array<ArrayBufferLike>,
     texture: GPUTexture,
   ) {
     // ... vertex buffer creation code ...

-    // bind group should be pre-created and reuse instead of constantly initialized
-    // TODO: avoid creatign bind group on every render
-    const bindGroup = device.createBindGroup({
-      layout: pipeline.getBindGroupLayout(0),
-      entries: [
-        { binding: 0, resource: { buffer: uniformBuffer }},
-        { binding: 1, resource: sampler },
-        { binding: 2, resource: texture.createView() },
-      ],
-    })
+    // Get or create bind group for this texture
+    let bindGroup = textureBindGroups.get(texture);
+    if (!bindGroup) {
+      bindGroup = device.createBindGroup({
+        layout: bindGroupLayout,
+        entries: [
+          { binding: 0, resource: { buffer: uniformBuffer }},
+          { binding: 1, resource: sampler },
+          { binding: 2, resource: texture.createView() },
+        ],
+      });
+      textureBindGroups.set(texture, bindGroup);
+    }
🧹 Nitpick comments (5)
src/WebGPU/programs/pickTexture/getProgram.ts (5)

27-34: Clarify vertex buffer layout with comments.

The vertex buffer layout is correctly defined but would benefit from clearer documentation about the purpose of each attribute in the picking pipeline.

      buffers: [
        {
          arrayStride: STRIDE * 4,
          attributes: [
-            {shaderLocation: 0, offset: 0, format: 'float32x4'},  // destination position
-            {shaderLocation: 1, offset: 16, format: 'float32x2'},  // source position
-            {shaderLocation: 2, offset: 16 + 8, format: 'float32'},  // id
-            // {shaderLocation: 3, offset: 16 + 8, format: 'float32x3'},  // id
+            {shaderLocation: 0, offset: 0, format: 'float32x4'},  // destination position (x, y, width, height)
+            {shaderLocation: 1, offset: 16, format: 'float32x2'},  // source position (u, v)
+            {shaderLocation: 2, offset: 16 + 8, format: 'float32'},  // asset id for picking

32-32: Remove commented-out code.

There's a commented-out line for a potential alternative representation of the ID value. Since you've settled on using float32 for the ID, this commented line can be removed.

            {shaderLocation: 1, offset: 16, format: 'float32x2'},  // source position
            {shaderLocation: 2, offset: 16 + 8, format: 'float32'},  // id
-            // {shaderLocation: 3, offset: 16 + 8, format: 'float32x3'},  // id

77-82: Consider reusing vertex buffers for improved performance.

Similar to the bind group issue, creating a new vertex buffer on every draw call can be inefficient. If the vertex data changes frequently, consider using a mapped buffer or double-buffering technique instead.

Since the vertex buffer creation pattern is similar to the bind group issue, you could implement a vertex buffer pool or cache as well. If the vertex data is static or changes infrequently, this optimization would be significant.


99-99: Remove commented-out unused code.

This commented line about translating the world projection matrix doesn't add value and should be removed to improve code clarity.

-    // const translateWorldProjMatrix = mat4.translate(worldProjectionMatrix, [])

86-86: Fix typo in TODO comment.

There's a typo in the TODO comment: "creatign" should be "creating".

-    // TODO: avoid creatign bind group on every render
+    // TODO: avoid creating bind group on every render
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between edcf703 and a6e1a24.

📒 Files selected for processing (2)
  • integration-tests/tests/creator.spec.ts (1 hunks)
  • src/WebGPU/programs/pickTexture/getProgram.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • integration-tests/tests/creator.spec.ts
🧰 Additional context used
🪛 ESLint
src/WebGPU/programs/pickTexture/getProgram.ts

[error] 7-7: 'debugPresentationFormat' is defined but never used.

(@typescript-eslint/no-unused-vars)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
src/WebGPU/programs/drawTriangle/getProgram.ts (1)

74-74: Vertex count flooring – fix accepted

The earlier review suggested replacing Math.round; your update applies the fix faithfully.

src/WebGPU/programs/pickTexture/getProgram.ts (2)

5-8: ⚠️ Potential issue

Remove unused parameter

The debugPresentationFormat parameter is defined but never used in the function body. There is a commented-out reference to it at line 41, but the actual implementation uses a hardcoded 'r32uint' format instead.

-export default function getProgram(
-  device: GPUDevice,
-  debugPresentationFormat: GPUTextureFormat
-) {
+export default function getProgram(
+  device: GPUDevice,
+) {
🧰 Tools
🪛 ESLint

[error] 7-7: 'debugPresentationFormat' is defined but never used.

(@typescript-eslint/no-unused-vars)


83-92: 🛠️ Refactor suggestion

Optimize bind group creation by caching

The code creates a new bind group for each draw call, which is inefficient as noted by the TODO comment. Implement a caching strategy to reuse bind groups for the same textures.

+  // Create bind group layout once
+  const bindGroupLayout = pipeline.getBindGroupLayout(0);
+
+  // Cache bind groups per texture
+  const textureBindGroups = new Map<GPUTexture, GPUBindGroup>();
+
   return function drawTexture(
     pass: GPURenderPassEncoder,
     worldProjectionMatrix: Float32Array,
     vertexData: Float32Array<ArrayBufferLike>,
     texture: GPUTexture,
   ) {
     // ...

-    // bind group should be pre-created and reuse instead of constantly initialized
-    // TODO: avoid creatign bind group on every render
-    const bindGroup = device.createBindGroup({
-      layout: pipeline.getBindGroupLayout(0),
-      entries: [
-        { binding: 0, resource: { buffer: uniformBuffer }},
-        { binding: 1, resource: sampler },
-        { binding: 2, resource: texture.createView() },
-      ],
-    })
+    // Get or create bind group for this texture
+    let bindGroup = textureBindGroups.get(texture);
+    if (!bindGroup) {
+      bindGroup = device.createBindGroup({
+        layout: bindGroupLayout,
+        entries: [
+          { binding: 0, resource: { buffer: uniformBuffer }},
+          { binding: 1, resource: sampler },
+          { binding: 2, resource: texture.createView() },
+        ],
+      });
+      textureBindGroups.set(texture, bindGroup);
+    }
🧹 Nitpick comments (13)
src/WebGPU/programs/drawTexture/getProgram.ts (2)

83-89: Avoid recreating the vertex buffer every draw call

A fresh GPU buffer is allocated and uploaded on every invocation. For sprites that are rendered every frame this becomes a considerable CPU-GPU sync point and GC pressure.
Consider:

-const vertexBuffer = device.createBuffer({ … })
-device.queue.writeBuffer(vertexBuffer, 0, vertexData)
+// allocate once outside and reuse:
+vertexBuffer.setSubData(0, vertexData) // or mapAsync-based ring buffer

A small ring/double buffer with COPY_DST | VERTEX usage is usually sufficient and keeps the draw call hot.


91-99: Bind group can be cached

Because the layout (and the texture view, assuming the same texture instance) are constant per drawTexture call, the bind group can be created once during program initialization and reused. Creating a bind group is cheap but still shows up in profiles when done thousands of times per frame.

src/WebGPU/programs/drawTriangle/getProgram.ts (3)

4-4: Keep STRIDE comment in sync with layout

const STRIDE = 4 + 4 matches the two float32x4 attributes, but the trailing comment (// + 1 + 1 + 4) is now misleading. Removing or updating it avoids confusion when the format evolves.


74-80: Reuse vertex buffer to cut per-frame allocations

Same remark as for drawTexture: allocating a brand-new vertex buffer per border is unnecessary. A transient ring buffer or a device-persistent buffer updated via queue.writeBuffer will remove this allocation hotspot.


82-89: Bind group creation every frame

Only the uniform buffer changes, not its binding. Cache the bind group once during getProgram to shave off WebGPU object churn.

src/WebGPU/pick.ts (2)

29-33: Dead depth texture – remove or wire-up

pickDepthTexture is created but never referenced (depth attachment is commented out). This allocates GPU memory for no benefit. Either supply it in depthStencilAttachment or delete the creation.


42-54: Unused debugRenderPass parameter triggers linter

debugRenderPass is declared but not used, causing an ESLint error (@typescript-eslint/no-unused-vars). If future debugging is planned, prefix the name with _ or gate it behind a build flag; otherwise remove the parameter.

src/WebGPU/programs/pickTexture/getProgram.ts (6)

28-33: Clean up commented code

There's a commented out shader location (line 32) that appears to be an alternative implementation or previous version of the ID attribute. Since you're now using a single float for the ID at location 2, this comment should be removed to avoid confusion.

            {shaderLocation: 0, offset: 0, format: 'float32x4'},  // destination position
            {shaderLocation: 1, offset: 16, format: 'float32x2'},  // source position
            {shaderLocation: 2, offset: 16 + 8, format: 'float32'},  // id
-            // {shaderLocation: 3, offset: 16 + 8, format: 'float32x3'},  // id

41-43: Clean up commented code and document format choice

The commented-out format line suggests you previously considered using the debugging format but decided on 'r32uint' instead. Consider adding a comment explaining why 'r32uint' is the appropriate format for the picking texture.

      entryPoint: 'fs',
      targets: [{
-        // format: debugPresentationFormat,
+        // Using r32uint to store asset IDs as unsigned integers
        format: 'r32uint',
      }],

45-52: Remove or document commented code

The commented out primitive and depth stencil configurations suggest this code might be incomplete or experimental. If these settings are unnecessary for the picking pipeline, remove them. If they might be needed in the future, document why they're commented out.

    },
-    // primitive: {
-    //   cullMode: 'back',
-    // },
-    // depthStencil: {
-    //   depthWriteEnabled: true,
-    //   depthCompare: 'less',
-    //   format: 'depth24plus',
-    // },

74-80: Consider reusing vertex buffers

Creating a new vertex buffer for each draw call can be expensive. Consider implementing a buffer pooling strategy or moving the buffer creation outside the draw function for cases where the same vertex data is used multiple times.


97-97: Remove or implement commented code

This commented line about translating the projection matrix should either be implemented or removed if not needed.

-    // const translateWorldProjMatrix = mat4.translate(worldProjectionMatrix, [])

1-4: Add documentation about picking functionality

This file implements GPU-based picking functionality which is a crucial part of the asset hover feature, but lacks documentation explaining how it works. Consider adding comments to explain the picking system's purpose, how it uses the r32uint format to store IDs, and how it integrates with the hover functionality.

+/**
+ * This module provides a WebGPU rendering pipeline for asset picking functionality.
+ * It renders textures with asset IDs encoded as r32uint values, which can then be
+ * read back to determine which asset is under the cursor.
+ * 
+ * The picking system works by:
+ * 1. Rendering assets with their IDs to an offscreen texture
+ * 2. Reading back the pixel value under the cursor
+ * 3. Using that ID to determine the hovered asset
+ */
 import shaderCode from "./shader.wgsl"
 
 const STRIDE = 4 + 2 + 1
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a6e1a24 and 91045ca.

📒 Files selected for processing (4)
  • src/WebGPU/pick.ts (1 hunks)
  • src/WebGPU/programs/drawTexture/getProgram.ts (1 hunks)
  • src/WebGPU/programs/drawTriangle/getProgram.ts (1 hunks)
  • src/WebGPU/programs/pickTexture/getProgram.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
src/WebGPU/programs/drawTriangle/getProgram.ts (5)
src/WebGPU/programs/drawTexture/getProgram.ts (1)
  • getProgram (6-112)
src/WebGPU/programs/pickTexture/getProgram.ts (1)
  • getProgram (5-105)
src/WebGPU/programs/draw3dModel/getProgram.ts (1)
  • getProgram (3-97)
src/WebGPU/programs/draw3dModelTexture/getProgram.ts (1)
  • getProgram (3-99)
src/WebGPU/programs/initPrograms.ts (1)
  • drawTriangle (10-10)
src/WebGPU/pick.ts (1)
src/WebGPU/pointer.ts (1)
  • pointer (1-1)
🪛 ESLint
src/WebGPU/pick.ts

[error] 40-40: 'debugRenderPass' is defined but never used.

(@typescript-eslint/no-unused-vars)

src/WebGPU/programs/pickTexture/getProgram.ts

[error] 7-7: 'debugPresentationFormat' is defined but never used.

(@typescript-eslint/no-unused-vars)

🔇 Additional comments (3)
src/WebGPU/programs/drawTexture/getProgram.ts (1)

82-82: Vertex count flooring implemented correctly

Replacing Math.round() with | 0 deterministically floors the division result and prevents the off-by-one issue we discussed earlier – nice follow-up!

src/WebGPU/programs/drawTriangle/getProgram.ts (1)

23-27: Attribute offsets look correct – just verify colour packing

Position (16 B) followed by colour (16 B) inside a 32 B stride lines up nicely; just ensure the shader expects a vec4<f32> colour.

src/WebGPU/pick.ts (1)

87-94: bytesPerRow omitted – verify on all implementations

You rely on the special-case “1 × 1 copy” path where bytesPerRow may be optional. Unfortunately, some adapters (notably older Dawn-based Chrome builds) still require an explicit, 256-aligned value. Safer variant:

-encoder.copyTextureToBuffer({ texture: this.pickTexture }, { buffer: this.pickBuffer }, { width: 1, height: 1 })
+const bytesPerRow = 256 // 1 pixel × 4 bytes, rounded up to 256
+encoder.copyTextureToBuffer(
+  { texture: this.pickTexture },
+  { buffer: this.pickBuffer, bytesPerRow, rowsPerImage: 1 },
+  { width: 1, height: 1 }
+)

Please test on Chromium/Edge + Firefox to confirm.

Comment on lines +97 to +105
async asyncPick(state: State) {
if (!this.isPreviousDone) return
this.isPreviousDone = false
await this.pickBuffer.mapAsync(GPUMapMode.READ, 0, 4 * NUM_PIXELS)
const [id] = new Uint32Array(this.pickBuffer.getMappedRange(0, 4 * NUM_PIXELS))
state.update_hover(id)
this.pickBuffer.unmap()
this.isPreviousDone = true
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Handle mapping failures and race conditions

mapAsync may reject (device lost, validation error). If that happens isPreviousDone stays false, freezing picking forever. Wrap in try/finally:

this.isPreviousDone = false
try {
  await this.pickBuffer.mapAsync(GPUMapMode.READ, 0, 4)
  
} finally {
  this.pickBuffer.unmap()
  this.isPreviousDone = true
}

Also consider using queue.onSubmittedWorkDone() to ensure the copy has finished before mapping, preventing undefined behaviour on slower GPUs.

@mateuszJS mateuszJS merged commit 918539d into next May 11, 2025
4 checks passed
@github-actions
Copy link

🎉 This PR is included in version 0.1.0-next.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants