From 79fd2f281f37348673a738c9b0046ceacfc9c63f Mon Sep 17 00:00:00 2001 From: Caio Viotti Date: Wed, 14 Oct 2026 13:11:21 -0300 Subject: [PATCH 1/3] Match reduced contrast lookup buffers to texture dimensions --- .../renderers/pixi-media-inspection-filter.ts | 1 + .../renderers/pixi-media-scene.push.test.ts | 70 +++++++++++++++++-- 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/packages/web/src/renderers/pixi-media-inspection-filter.ts b/packages/web/src/renderers/pixi-media-inspection-filter.ts index 8ef10225..7a5a053b 100644 --- a/packages/web/src/renderers/pixi-media-inspection-filter.ts +++ b/packages/web/src/renderers/pixi-media-inspection-filter.ts @@ -184,6 +184,7 @@ export function createPixiMediaInspectionFilter( pixels[i * 4 + 3] = 255; } uniforms.uTiles.set([tilesX, tilesY]); + lutSource.resource = pixels.subarray(0, luts.length * 4); lutSource.resize(256, tilesX * tilesY); lutSource.update(); hasHistogram = true; diff --git a/packages/web/src/renderers/pixi-media-scene.push.test.ts b/packages/web/src/renderers/pixi-media-scene.push.test.ts index 03be63ee..96627266 100644 --- a/packages/web/src/renderers/pixi-media-scene.push.test.ts +++ b/packages/web/src/renderers/pixi-media-scene.push.test.ts @@ -68,7 +68,9 @@ interface PaintedSprite { // Push mode asks Pixi for WebGPU, so the mock renderer carries a device: the // scene then composites through the GPU path production runs, not the staging // canvas fallback. -vi.mock("pixi.js", () => { +vi.mock("pixi.js", async (importOriginal) => { + const { BufferImageSource } = + await importOriginal(); class Stub { destroy = vi.fn(); } @@ -341,8 +343,6 @@ vi.mock("pixi.js", () => { class Destroyable { destroy = vi.fn(); - resize = vi.fn(); - update = vi.fn(); style = {}; } @@ -355,6 +355,7 @@ vi.mock("pixi.js", () => { return { destroy: vi.fn(), resources: { + ...options.resources, inspectionUniforms: { uniforms: Object.fromEntries( Object.entries(definitions).map(([name, definition]) => [ @@ -385,7 +386,7 @@ vi.mock("pixi.js", () => { Application, Assets: { load: vi.fn(), unload: vi.fn() }, BlurFilter: Stub, - BufferImageSource: Destroyable, + BufferImageSource, CanvasSource, ColorMatrixFilter, Container, @@ -1280,6 +1281,13 @@ describe("push-presented Pixi scene", () => { ); expect(filtered).toHaveLength(1); expect(filtered[0]?.filters).toHaveLength(2); + const inspection = filtered[0]?.filters?.[0] as { + resources: { inspectionUniforms: { uniforms: Record } }; + }; + expect(inspection.resources.inspectionUniforms.uniforms).toMatchObject({ + uGamma: 2, + uAuto: 0, + }); await applyDisplayAdjustment(scene, { gamma: 2, @@ -1287,6 +1295,10 @@ describe("push-presented Pixi scene", () => { autoContrast: true, }); expect(scene.getRenderCount?.()).toBe(original + 2); + expect(inspection.resources.inspectionUniforms.uniforms).toMatchObject({ + uGamma: 2, + uAuto: 1, + }); expect(pixiMock.displayFilters.at(-1)).toMatchObject({ contrast: expect.any(Function), }); @@ -1315,6 +1327,56 @@ describe("push-presented Pixi scene", () => { scene.destroy(); }); + it.each([ + { width: 3, height: 2, tiles: 6 }, + { width: 2048, height: 1, tiles: 8 }, + ])( + "uploads complete Auto lookup-table rows for $width×$height images", + async ({ width, height, tiles }) => { + const { createPixiMediaScene } = await import("./pixi-media-scene"); + const { gpuUploadBufferImageResource } = + await vi.importActual("pixi.js"); + const scene = await createPixiMediaScene({ + ...createSceneOptions(undefined), + stillFrame: true, + }); + scene.initializeMedia({ width, height }); + scene.presentSample({ + timestamp: 0, + duration: 0, + draw: vi.fn(), + close: vi.fn(), + }); + await applyDisplayAdjustment(scene, { gamma: 2, autoContrast: true }); + const sprite = pixiMock.sprites.find( + (candidate) => candidate.filters?.length, + ); + const inspection = sprite?.filters?.[0] as { + resources: { uLut: import("pixi.js").BufferImageSource }; + }; + const source = inspection.resources.uLut; + const writeTexture = vi.fn(); + const gpu = { + device: { queue: { writeTexture } }, + } as unknown as Parameters[2]; + gpuUploadBufferImageResource.upload(source, {} as GPUTexture, gpu); + const [, resource, layout, size] = writeTexture.mock.calls[0]!; + expect(resource).toBe(source.resource); + expect(layout).toEqual({ + offset: 0, + rowsPerImage: tiles, + bytesPerRow: 256 * 4, + }); + expect(size).toEqual({ + width: 256, + height: tiles, + depthOrArrayLayers: 1, + }); + expect(source.resource.byteLength).toBe(256 * tiles * 4); + scene.destroy(); + }, + ); + it("keeps the media on the staging canvas when the device refuses a decoded frame", async () => { // The refusal lands on the capability probe, which is the first copy the // scene asks for. Firefox refuses every one after it too, and the point of From 897ff26331c6370c406c0e98715d7e62e26925e7 Mon Sep 17 00:00:00 2001 From: Caio Viotti Date: Wed, 14 Oct 2026 15:53:56 -0300 Subject: [PATCH 2/3] Prepare supervision 0.2.9 release metadata --- docs/public/guides/migrating-to-0.2.md | 4 ++-- docs/public/typedoc-icons.js | 2 +- package-lock.json | 2 +- packages/web/package.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/public/guides/migrating-to-0.2.md b/docs/public/guides/migrating-to-0.2.md index 7103cfce..e71e1ce0 100644 --- a/docs/public/guides/migrating-to-0.2.md +++ b/docs/public/guides/migrating-to-0.2.md @@ -6,10 +6,10 @@ summary: Update playback gates, playback feedback, and interaction highlights fr # Migrating to 0.2 -Upgrade to `0.2.8` with: +Upgrade to `0.2.9` with: ```sh -npm install supervision@0.2.8 +npm install supervision@0.2.9 ``` The engine remains a subpath of `supervision`; do not add a separate engine or diff --git a/docs/public/typedoc-icons.js b/docs/public/typedoc-icons.js index 707611cb..c17299b2 100644 --- a/docs/public/typedoc-icons.js +++ b/docs/public/typedoc-icons.js @@ -2,7 +2,7 @@ (function () { const packageName = "supervision"; - const packageVersion = "0.2.8"; + const packageVersion = "0.2.9"; const packageReleaseStatus = ""; const kindIconMap = { Accessor: "A", diff --git a/package-lock.json b/package-lock.json index 1991df60..94eb5887 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10791,7 +10791,7 @@ }, "packages/web": { "name": "supervision", - "version": "0.2.8", + "version": "0.2.9", "license": "MIT", "dependencies": { "mediabunny": "^1.55.5", diff --git a/packages/web/package.json b/packages/web/package.json index 790511a4..93c42b24 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "supervision", - "version": "0.2.8", + "version": "0.2.9", "description": "Browser-native computer vision media rendering and annotation engine.", "repository": { "type": "git", From 8f05e38f87590ebcace38ab78c8095d32143ed12 Mon Sep 17 00:00:00 2001 From: Caio Viotti Date: Wed, 14 Oct 2026 16:03:38 -0300 Subject: [PATCH 3/3] Test empty display adjustment reset after Gamma and Auto --- .../web/src/renderers/pixi-media-scene.push.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/web/src/renderers/pixi-media-scene.push.test.ts b/packages/web/src/renderers/pixi-media-scene.push.test.ts index 96627266..d9ad6f5a 100644 --- a/packages/web/src/renderers/pixi-media-scene.push.test.ts +++ b/packages/web/src/renderers/pixi-media-scene.push.test.ts @@ -1324,6 +1324,17 @@ describe("push-presented Pixi scene", () => { contrast: ReturnType; }; expect(manual.contrast).toHaveBeenCalledWith(0.8, true); + await applyDisplayAdjustment(scene, { gamma: 2, autoContrast: true }); + expect(scene.getRenderCount?.()).toBe(original + 4); + await applyDisplayAdjustment(scene, {}); + expect(scene.getRenderCount?.()).toBe(original + 5); + const identity = pixiMock.displayFilters.at(-1) as { + brightness: ReturnType; + contrast: ReturnType; + }; + expect(filtered[0]?.filters).toStrictEqual([identity]); + expect(identity.brightness).toHaveBeenCalledWith(1, false); + expect(identity.contrast).toHaveBeenCalledWith(0, true); scene.destroy(); });