refactor(render): WebGPU rewrite + legacy WebGL2 renderer cutover - #61
Conversation
- Mark s0-foundation in_progress - Add Slice 5.5 backend adapter to bridge WebGL2 → WebGPU one-point switch - Add render result contract, cache key versioning, stage choreography boundary decisions - Defer ASCII production parity from Slice 1 to Slice 6 (depends on develop chain)
Adds `src/lib/gpu/` — context lifecycle, texture pool, WGSL shader cache, linear pass executor, and the passthrough render pass that gates the slice. - `context.ts`: requestAdapter → requestDevice with feature detection, lost-device fanout, idempotent dispose - `resources.ts`: w×h×format-keyed texture pool with LRU eviction by free count + bytes; image upload via copyExternalImageToTexture; padded readback to a tight Uint8Array - `shaders.ts`: WGSL module cache keyed by FNV-1a hash of source - `pipeline.ts`: linear executor over a single GPUCommandBuffer, ping-pongs pool textures, releases AFTER submit so encoded attachments stay valid until the queue runs them - `passes/types.ts`: GPUPass discriminated union (render | compute); render passes use a bindGroups factory because the input view changes per frame as the executor ping-pongs - `passes/utility/passthrough.ts` + `wgsl/passthrough.wgsl`: identity pass with Y-invariant UV (kills the WebGL2 even-pass parity hack) Validation: `scripts/gpu-smoke/passthrough.html` smoke harness runs the foundation end-to-end against a real GPUDevice. On a 16×16 gradient fixture, readback max per-channel diff = 0 — exactly within the 1/255 tolerance gate. Adds @webgpu/types; ambient types and *.wgsl?raw module declaration in src/lib/gpu/webgpu.d.ts. knip entry list expanded with the new tree until Slice 5.5 wires consumers. Closes s0-foundation in docs/tasks/render-kernel-webgpu-rewrite.json.
Addresses three real bugs and two rule violations from the post-Slice-0 review pass. Real bugs: - TexturePool.release(handle) let a stale handle invalidate a re-leased entry — the closure-based handle.release() was already generation-safe but the public method was the unguarded path. Removed the public method; PipelineExecutor now calls lease.release() exclusively. New test guards stale-handle release. - PipelineExecutor silently no-op'd canvasOutput when the last enabled pass was compute (canvas would never be written, caller would still see output: null and assume success). Now throws fast. - GPUContext.onLost handlers registered after device loss never fired — the lost promise had already resolved. Late registrations now dispatch via the resolved promise. Rule violations: - GPURenderPassDescriptor.consumesPrior was dead metadata: documented for "Slice 1+ executor validation" but never read. Per AGENTS.md "do not add aliases ... unless they add a real invariant or boundary," dropped. Slice 1 reintroduces it together with the enforcement site. - @public — consumed by ... tags on GPU exports were no-ops because knip.json already lists src/lib/gpu/**/*.ts as an entry pattern. Removed the eight redundant tags; knip entry config is the single source of truth. Coverage gap closed: pipeline.test.ts now asserts (a) each pass's priorInputView is the prior pass's output, and (b) no pool texture is re-leased mid-execute (the release-after-submit invariant).
- PipelineExecuteResult is now a discriminated union of
{ kind: "skipped" | "canvas" | { kind: "texture"; output } } so
callers can no longer collapse "no surface produced" and "canvas was
written" into the same `output: null` case. Slice 5.5's
{ status, surface, fallbackReason? } contract maps onto this without
ambiguity.
- ShaderCache no longer hashes via FNV-1a (32-bit collision risk on
large entry counts and silent wrong-module return on collision).
Map<string, GPUShaderModule> with the full source as key dedupes by
value with no collision risk; same memory shape since the source was
already stored in each entry. Tests and behavior unchanged.
Smoke harness re-run on real GPU: max per-channel diff = 0, PASS.
Compute-driven ASCII rendering with structure-aware glyph selection. CPU descriptor extraction (27 floats per glyph: density + 4×4 sub-grid + 8-bin unsigned-orientation gradient histogram + centroid) feeds an analysis → selection → composition pass chain that lives alongside the existing WebGL2 carrier behind src/lib/gpu/. structureWeight (0–1) blends density-only selection (matches the WebGL2 carrier) with structure-only matching. Validation gates green on SwiftShader via scripts/gpu-smoke/ascii.html: density-step bands match closest-density glyph within ±1 atlas slot, and self-rendered directional fixtures recover their own glyph under structureWeight=1. <16ms timing gate is asserted on real GPUs only; informational on fallback adapters.
- selection.wgsl: divide centroidDist by 2 so the structure-blend term contributes on the same per-element scale as subgridDist/edgeDist (the comment claimed normalization but the code wasn't doing it). - ascii pass caches: convert createPass into a method so device can stay private; the prior free-function shape forced a public device leak only to satisfy the bind-group builder. - composition.ts: bake the bind group at factory time. The bound resources are stable across frames, so the executor's per-frame `bindGroups(ctx)` callback now returns a constant — closes the per-frame allocation churn and the latent stale-ref-via-closure concern. - composition: drop COMPOSITION_UNIFORMS_BYTE_SIZE from 48 to the WGSL struct's actual 40 bytes; the slack was misleading vs the WGSL truth. - types.ts: delete pre-existing orphan AsciiGpuCarrierInput interface (knip-flagged dead code; no importers). - slice JSON: honestly word the timing gate — <16ms is real-GPU only; fallback adapters are informational and a real-GPU run is still pending. ascii.html harness re-validated on SwiftShader: density-step + directional fixtures still green.
…ter/OutputEncode)
Ports the four photographic-core fragment shaders to WGSL render passes
and validates each against the existing WebGL2 GLSL output. Shared
fullscreen vertex stage and color-space helpers (sRGB↔linear, LMS,
OKLab, hsv-fast, luminance) live in `wgsl/lib/`; pass shaders are
concatenated with the libs in TypeScript at compile time. Each pass
with parameters returns a `*PassHandle = { descriptor, updateParams,
destroy }` so the uniform GPUBuffer is reusable once the orchestrator
lands in Slice 5.5.
Geometry's per-pixel out-of-bounds early-return puts subsequent
`textureSample` calls in non-uniform control flow, which WGSL forbids.
Switched to `textureSampleLevel(..., 0.0)` — same behavior on a
single-mip source.
Validation harness `scripts/gpu-smoke/photoCore.html` +
`webgl2Reference.ts` runs 14 scenarios (1 InputDecode, 4 OutputEncode,
4 Geometry, 5 Master). All clear `maxDiff=0/255` against the WebGL2
reference; gate `< 2/255` clears with margin. Validated on SwiftShader
fallback adapter; real-GPU run pending on user hardware.
…ly skipping Swallowing a null getUniformLocation hid test-configuration mismatches. Failing fast surfaces shader/uniform-map mismatches during smoke-test authoring. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ports HSL (8-channel OKLab hue/sat/lum + calibration + B&W), Curve (two-stage 256-entry LUT), and Detail (texture/clarity/sharpening/NR) to WGSL. All 11 parity scenarios hit maxDiff=0/255 on SwiftShader. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…in/Effects) - Five WGSL shaders in src/lib/gpu/wgsl/film/ porting FilmPrepUber, FilmColorLutUber, FilmPrintUber, ProceduralGrain (both grain models), and FilmEffectsUber from GLSL - Five TS pass factories in src/lib/gpu/passes/film/ following the PipelineCache + PassHandle pattern from develop passes; placeholder helpers for 3D LUT slots - Extend webgl2Reference.ts with ExtraTexture3D support (texImage3D path) - Validation harness scripts/gpu-smoke/filmPipeline.html covering 20 scenarios across all five passes (gate ≤ 2/255 vs WebGL2) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- print.wgsl: fix transposed default stock matrix (stock≤0.5 case was using row vectors instead of column vectors vs GLSL mat3 layout) - print.wgsl: add missing `&& q.print_params.w > 2.5` guard on print LUT condition, matching GLSL's `u_printLutEnabled && u_printStock > 2.5` - passes/film/utils.ts: extract shared createPlaceholderLut3D helper - colorLut.ts / print.ts: import from utils, remove duplicate definitions - filmPipeline.html: bind placeholder sampler3D textures for colorLut scenarios 0–2 and all print scenarios (was leaving default unit 0 as a 2D texture, technically undefined behavior for sampler3D) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements mask/ (brushStamp, linearGradient, maskedBlend, maskInvert, radialGradient, rangeGate), post/ (halationThreshold/Composite, glowThreshold/Composite), and utility/ (gaussianBlur, bilateralScale, downsample, dilate, layerBlend) WGSL passes. Validation: 29 scenarios across 15 pass types, maxDiff=0/255 on SwiftShader fallback (scripts/gpu-smoke/maskingPost.html). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces RenderBackend interface and BackendRenderResult contract in src/render/image/. WebGPURenderBackend delegates to src/lib/gpu/orchestrator; all 682 existing tests pass unchanged with the new indirection in place. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wires WebGPURenderBackend as the default in renderSingleImage.ts. Adds orchestrator.ts (named-step sequence: fetchOrComputeSource/applyGeometry/ runPipeline/composeLocal/produceSurface), cacheKeys.ts (v1 schema-versioned builder), and lutLoader.ts. Test mocks updated from imageProcessing to orchestrator module. All 682 tests pass; tsc clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ions Removes all \"mirrors .frag\", \"replaces WebGL2\", and similar stale WebGL2 references from src/lib/gpu/ pass docstrings. Closes renderer-y-convention-unification task (solved by design in WGSL path). Updates decisions.md: WebGPU backend, Y-axis convention, cache key policy. s7 full deletion of src/lib/renderer/ deferred to media-native-render-pipeline. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All 5 slices done. Migrate load-bearing decisions to docs/decisions.md: pipeline stage order, authored family split (carrierTransforms/signalDamage/ semanticOverlays/motionPrograms), RenderQualityTier, AnalysisLayerInputs, MotionProgram contract. Also updates Backend note: PipelineRenderer removal no longer tied to a specific task. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Plan the carrier/overlay/effect WebGPU execution-layer cutover: 7 slices (halftone, channel-drift, filter2d, effect-mask, overlay, ASCII wiring, legacy delete). s1-s6 are independent per-consumer swaps; s7 deletes src/lib/renderer/ + twgl.js and closes render-kernel-webgpu-rewrite s7 as a side effect. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Port HalftoneCarrier.frag to wgsl/carrier/halftone.wgsl + standalone surface adapter applyHalftoneOnSurface. halftoneEffect.ts switched off src/lib/renderer/gpuHalftoneCarrier (deleted). WGSL mat2x2 column order matches GLSL's column-major mat2 exactly. Uniform layout packs canvasSize/freq/angle/shape/colorMode/scale/contrast + bgColor.rgba + invert flag in 4 vec4. tsc clean, 682/682 tests pass. Smoke harness scripts/gpu-smoke/halftone.html covers 9 scenarios across mono/cmyk/rgb × shape × invert × bg-opacity; real-GPU run pending. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Real bug: upload texture and pass handle leaked when executor.execute threw. Hoist both into outer-scope handles, dispose in finally. - Rule violation: drop empty-extension alias ApplyHalftoneOnSurfaceInput; use HalftonePassParams directly in ApplyHalftoneOnSurfaceOptions and in halftoneEffect.ts. - Rule violation: unexport orchestrator-shape internals (HalftonePipelineCache, createHalftonePass, HalftonePassOptions, HalftonePassHandle, HalftoneShape, HalftoneColorMode) — halftone is not currently composed into the kernel orchestrator, so these have no external consumer and were tripping knip. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Port ChannelDrift.frag to wgsl/signalDamage/channelDrift.wgsl + standalone surface adapter applyChannelDriftOnSurface. signalDamageExecution.ts switched off src/lib/renderer/gpuSignalDamage (deleted). Pruned channelDrift program entry from ProgramRegistry; removed renderChannelDriftComposite from PipelineRenderer. Uniform layout: 3 vec4 = 48 bytes packing canvas/intensity, redGreen offsets, blue offsets. tsc clean, 682/682 tests pass. Smoke harness scripts/gpu-smoke/channelDrift.html covers 6 scenarios across pos/neg/ asymmetric/diagonal/zero-intensity/zero-offsets; real-GPU run pending.
- Rule violation: drop unjustified orchestrator-shape exports
(ChannelDriftPipelineCache, createChannelDriftPass) — knip's gpu/**
entry already covers the file, and matching s1 halftone's post-review
surface keeps the public API to {ChannelDriftPassParams,
ApplyChannelDriftOnSurfaceOptions, applyChannelDriftOnSurface}.
- Rewire scripts/gpu-smoke/channelDrift.html to exercise
applyChannelDriftOnSurface directly (constructs a synthetic
RenderSurfaceHandle and reads pixels back from result.sourceCanvas);
this also gains coverage of the surface-adapter boundary that
production goes through.
Sampler-leak finding rejected: GPUSampler has no destroy() per
@webgpu/types; samplers are GC-managed.
tsc clean, 682/682 tests pass.
Replaces the CPU buildAsciiCellGrids tone path + legacy WebGL2 carrier
helper with the WGSL adapter from s6c. asciiEffect.ts now passes
sourceCanvas + tone-normalization params (brightness/contrast/density/
coverage/edge/invert/dither) directly to applyAsciiCarrierOnSurface; cell
color/tone are computed on the GPU.
Deletes:
- src/lib/renderer/gpuAsciiCarrier.ts
- src/lib/renderer/shaders/AsciiCarrier.frag
- src/lib/renderer/shaders/templates/asciiCommon.glsl
- PipelineRenderer.{renderAsciiCarrierComposite,renderAsciiCarrierLayer,
renderAsciiBackgroundSourceLayer,uploadCellRgbaTexture,uploadCellR8Texture,
getGlyphAtlas,pruneAsciiGlyphAtlasCache,releaseAsciiGlyphAtlasRecord} +
asciiGlyphAtlasCache/Lru fields + emptyMaskTexture field/init/dispose +
AsciiCarrierGpuInput import + AsciiGpuCarrierInput/GlyphAtlasInput/
AsciiBackgroundSourceInput/AsciiGlyphAtlasRecord/AsciiAtlasDebugSnapshot/
AsciiLayerKind types
- ProgramRegistry asciiCarrier program entry (interface field, fragment
source row, deferred warmup entry, frag import, asciiCommon.glsl import,
ASCII_COMMON_MARKER + injectAsciiCommon helpers)
Vitest: 134 files / 672 passed (lost the asciiCarrier lazy-cache test in
ProgramRegistry.test.ts; consumer-side asciiEffect tests rewired to mock
the new module path).
Drops the in-file VSOut struct + vs_main entry point from composition.wgsl and switches composition.ts to concatenate wgsl/lib/fullscreen.wgsl ahead of the fragment source — same pattern as every other render pass in passes/* (channelDrift, develop/*, film/*, mask/*, overlay/*, post/*, utility/*, halftone, passthrough). Composition was the lone holdout from s1's ASCII slice 1 scaffolding. The local UV math (`(1.0 - p.y) * 0.5`) was a verbatim copy of the shared lib, so the swap is behavior-preserving.
…tureWeight
Three findings from the s6 review pass:
1. (real bug) applyAsciiCarrierOnSurface returned null when both
hasBackground and hasForeground were false (e.g. backgroundMode=none +
foregroundOpacity=0 + gridOverlay=false), and the lone caller
applyImageCarrierTransforms throws on null. Legacy
renderAsciiCarrierComposite presented base unchanged in the same
no-op state. Now returns the input surface; null stays reserved for
actual GPU failures.
2. (rule violation, hotspot regression) prepareAsciiGlyphSet rasterised
~70 glyphs into a Canvas2D, recomputed 27-float HOG descriptors, and
re-uploaded the atlas texture + descriptor buffer on every render.
Legacy PipelineRenderer.getGlyphAtlas cached by (font, charset).
Adds a Map<string, CachedGlyphSet> to the per-device cache keyed on
"${fontFamily}|${charset.join('')}" — atlas + descriptors now
build once per unique key and live for the device's lifetime.
3. (rule violation) types.ImageAsciiCarrierTransformParams.structureWeight
JSDoc claimed it was "consumed by the WebGPU ASCII pipeline" but the
adapter hardcoded `structureWeight: 0` on the selection uniform.
Now plumbed through AsciiCarrierSurfaceParams; selection.wgsl runs the
author-specified blend (still 0 by default — matches density-sorted
charset assumption).
…tProcessing / carrierAdjustments All four flagged by knip as unused; no production callers.
Most of imageProcessing.ts (3240L) was the legacy renderWithPipeline / RenderManager / PipelineRenderer driver. Real entry point is renderSingleImageToCanvas → WebGPURenderBackend → @/lib/gpu/orchestrator; nothing in src reaches the legacy path. - Move RenderImageStageDebugInfo / RenderImageStageResult / RenderImageStageSurfaceResult / RenderImageOptions / BackendRenderOptions into renderBackend.ts (only what the WebGPU orchestrator actually populates: stageId + surface + boundaries). - New src/lib/renderMode.ts holds the RenderMode union; 11 pass / surface / intent files updated to import from it. - renderIntent imports RenderQualityProfile directly from imageProcessingKeys. - Drop releaseRenderSlots; renderCanvasDocument no longer calls it (no equivalent in WebGPU — each pass owns its TexturePool per call). - Delete imageProcessing.ts, imageProcessing.debug.test.ts (entire test suite was for the deleted dev pipeline), rename aspectRatio test to imageProcessingKeys.aspectRatio.test.ts. tsc clean; vitest 656/656 pass.
After WebGL2 develop pipeline removal, only WebGPU paths still consume
these helpers — move them next to their real home so renderer/ can be
deleted wholesale.
- src/lib/renderer/CubeLUTParser{,.test}.ts → src/lib/gpu/cubeLut{,.test}.ts
- src/lib/renderer/types.ts → src/lib/gpu/uniformTypes.ts (uniform interfaces only)
- src/lib/renderer/uniformResolvers.ts → src/lib/gpu/uniformResolvers.ts
- New src/lib/gpu/curveLut.ts folds buildCurveLutPixels (gpu/CurveLut.ts) +
encodeCurveLutToHalfFloats (CurveLutEncoding.ts) — drops byte encoder /
identity helper / CURVE_LUT_SIZE which were only used inside renderer/.
Imports updated in orchestrator, lutLoader, imageProcessingKeys.
tsc clean; vitest 656/656 pass.
s7 closeout. After s1–s6 ported every consumer to WGSL and the WebGL2
develop pipeline was removed from imageProcessing, nothing reaches
src/lib/renderer/ anymore.
Removed:
- Entire src/lib/renderer/ tree (PipelineRenderer + FilterPipeline +
ProgramRegistry + PassBuilder + PassUniformUpdaters + RenderManager +
RenderPostProcessing + TextureManager + TexturePool + UniformManager +
MultiscaleDenoise + LUTCache + LUTLoader + CurveLut + CurveLutEncoding +
TiledRenderer + viewportRegion + reportGlError + opticsPasses +
gpuSurfaceOperation + every shaders/{*.frag,*.vert,*.glsl,templates}).
- src/glsl.d.ts (no remaining .frag/.vert/.glsl ?raw imports).
- src/types/renderer.ts (RendererMode / RendererCacheSnapshot /
TexturePoolMetrics — all knip-flagged unused; re-export pruned from
src/types/index.ts).
- twgl.js dependency from package.json + lockfile (only consumer was
src/lib/renderer/).
Also cleaned up the dead WebGL2-pipeline cache-key helpers in
src/lib/imageProcessingKeys.ts (createMasterKey / createHslKey /
createCurveKey / createDetailKey / createFilmKey / createOpticsKey /
createGeometryKey / createUploadKey / serializeCurvePoints + their
toNumberKey / hashString / applyOpticsToPassthroughGeometryUniforms
backing fns); only the geometry uniform builders remain — they are still
consumed by the WebGPU orchestrator.
renderIntent.ts now imports RenderQualityProfile from imageProcessingKeys
directly (the imageProcessing.ts re-export shim is gone).
Build-config cleanup: .gitignore / knip.json / eslint.config.js no
longer reference the removed src/lib/renderer/shaders/generated path.
tsc --noEmit clean; pnpm vitest run 638/638 pass (was 656 — the diff is
the 18 ProgramRegistry / PassBuilder / TiledRenderer / RenderManager /
RenderPostProcessing / gpuSurfaceOperation tests which evaporate with
their subjects; pnpm dead-code surfaces no new findings tied to s7.
…write Both task pairs deleted per AGENTS long-task closure rule. Load-bearing decisions migrated into docs/decisions.md: - Backend section now records WebGL2 path is fully removed (renderer/, twgl, .frag/.vert/.glsl, imageProcessing.ts dead pipeline) and that per-device caching is per-adapter via gpu/perDeviceCache.ts (no global renderer instance). - ASCII section records the two-stage compute chain (analysis → toneNormalize → selection) and Floyd-Steinberg → Bayer 8x8 substitution decision. - Brush mask cap drops the legacy GPU_BRUSH_MASK_MAX_POINTS constant reference; the 512 limit lives inline in passes/mask/localShape.ts. - WGSL surface-adapter section enumerates all current adapters; the 16F-vs-8-bit divergence note is reframed as 'no baseline' since the old WebGL2 path is gone.
Three real bugs surfaced by independent review of legacy-renderer-cutover
s7 (the previous commits used `tsc --noEmit` against the root tsconfig
which has `files: []` and skipped all source — `pnpm typecheck` running
`tsc -b` is the correct gate; baseline pre-existing errors aside, two
were s7 regressions and a third broke the build/dev script chain).
1. RenderImageStageDebugInfo lost `status` and `activePasses` in the
renderBackend.ts rewrite, but renderSingleImage.ts:53-54
computeTraceSignature still reads them, and the debug trace tests in
renderSingleImage.test.ts assert their values via mocks. Restored both
fields plus the BackendRenderStatus union (which moved up alongside
the type that consumes it).
2. scripts/generate-shaders.ts was wired into `pnpm dev`, `pnpm build:client`,
and `pnpm verify`, but its only inputs/outputs were under
src/lib/renderer/shaders — both deleted in the previous commit. Every
build / dev / verify command failed at `ERR_MODULE_NOT_FOUND`. Deleted
the script + the package.json entry; stripped the `pnpm run
generate:shaders &&` prefix from dev/build:client/verify.
3. scripts/gpu-smoke/{halftone,channelDrift,filter2dAdjust,overlay,filmPipeline,maskingPost,photoCore,photoExtended}.html
plus the shared scripts/gpu-smoke/webgl2Reference.ts all imported
`/src/lib/renderer/shaders/*.frag?raw` for the WebGL2 reference path.
Per docs/decisions.md (revised in the previous commit) all .frag/.vert/
.glsl source is gone — these harnesses 404 on load. Per the README
they're "one-off manual checks tied to the Slice validation gates";
every slice closed, so deleted the 8 broken HTML harnesses + the
shared WebGL2 helper, kept ascii.html + passthrough.html (WebGPU-only),
and updated the README to reflect that no WebGL2 reference baseline
remains.
pnpm typecheck: only the 5 pre-existing baseline errors remain (3 in
src/lib/gpu/passes/carrier/ascii/index.ts, 2 in src/lib/gpu/passes/mask/
rangeGate.ts) — all predate s7, none introduced by it.
pnpm vitest run: 638/638 pass.
…closeout s7 declared 'tsc/lint/vitest/dead-code all clean' as its closure gate but six pre-existing failures (predating s7, surfaced once 'pnpm typecheck' became the actual gate instead of 'tsc --noEmit') were left untouched. Clearing them now so the closure condition holds. - src/lib/gpu/passes/carrier/ascii/descriptors.ts: tighten descriptors field from Float32Array to Float32Array<ArrayBuffer> — the buffer is always a fresh ArrayBuffer (line 212 `new Float32Array(N)`), and the narrower type satisfies GPUAllowSharedBufferSource which writeBuffer requires under TS 5.7+ stricter typed-array generics. - src/lib/gpu/passes/carrier/ascii/index.ts: change `compositedLease` initializer from `null` to `null as PooledTexture | null` so TS control-flow analysis doesn't narrow the closure-mutated variable to `never` after the early-return guard. - src/lib/gpu/passes/mask/rangeGate.ts: tighten referenceSource / maskSource params from CanvasImageSource to ExternalImageSource — uploadExternalImageToTexture rejects SVGImageElement at runtime; the type now matches reality. - src/render/image/effectMask.ts + effectMask.test.ts: chain through the same ExternalImageSource narrowing. - src/lib/gpu/passes/develop/curve.ts: drop unused empty `CurvePassParams` interface (no consumers; was a placeholder for symmetry with other passes). pnpm typecheck clean; pnpm lint clean; pnpm vitest run 638/638 pass.
CI reviewer caught .github/workflows/ci.yml still calling `pnpm run generate:shaders` in the test (line 38) and typecheck (line 54) jobs. The script + package.json entry were deleted in 54ce144 but the CI hooks were missed; both jobs would now fail with ERR_PNPM_NO_SCRIPT. No replacement step needed — the script's only outputs lived under src/lib/renderer/shaders/generated/, which the legacy renderer deletion removed entirely. Build job is fine (uses pnpm build → build:client, already cleaned in 54ce144).
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 270ccd6799
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
P1 — context.ts: requestGPUContext was creating a fresh GPUDevice on every call, including each surface adapter invocation (~10 sites) plus every orchestrator render. createPerDeviceCache's WeakMap therefore missed every frame and devices accumulated unreleased. Added a process-wide singleton that returns the same context across calls and rebuilds only on device-lost (intentional dispose or accidental loss). No surface adapter needs to call ctx.dispose() — the device is shared. P2 — ascii/index.ts: backgroundMode === "blurred-source" with backgroundBlurPx <= 0.001 fell through to the transparent-placeholder branch but useBackgroundCanvas stayed true, so composition sampled an empty texture and the user saw a blank background instead of the "no-blur source" behaviour. Added a zero-blur branch that binds the original srcView directly; placeholder remains for the non-blurred-source modes where composition won't sample it. pnpm typecheck / lint / vitest 638/638 all clean.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ab3816a5de
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
P1 — orchestrator.{renderDevelopBase,renderFilmStage,renderFull} were
calling loadImageSource with only { signal }, dropping the bitmap LRU
cache. Each preview frame re-decoded the source via fetch +
createImageBitmap. Now passes through options.sourceCacheKey + useCache
so blob sources hit _sourceBitmapCache; canvas sources still no-op the
cache (loadImageSource short-circuits there).
P2 — srcTex.destroy() + loaded.cleanup?.() lived on the happy path, so
any throw between upload and the call to produceSurface would leak the
GPU texture and the source ImageBitmap / object URL. Moved both into
each function's outer finally; declared and as let
above the try and use optional chaining in finally.
Also dropped gpuCtx.dispose() from all three finally blocks — context.ts
holds a process-wide singleton (P1 fix in ab3816a). Calling dispose()
per render destroyed the device and forced re-creation on the next
frame, reverting the singleton to the broken pre-fix behaviour. Surface
adapters never disposed; orchestrator was the lone offender.
pnpm typecheck / lint / vitest 638/638 clean.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a16a4d2065
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ages options.signal was only consumed by loadImageSource; the rest of the GPU pipeline (texture upload, multi-pass execute, readback) ran to completion even after the request was cancelled, accumulating wasted work during preview drag. Added local throwIfAborted helper and inserted checkpoints at the natural pipeline boundaries: after upload, after each execute() that produces a major texture, and at the start + end of produceSurface (around readback). Push back on the companion P1 (strictErrors fallback): the backendStatus union (rendered / partial-fallback / kept-stale) is defined in renderBackend.ts but never read by renderSingleImage. A silent fallback in orchestrator without a kept-stale handler upstream would let the caller materialize a stale or partially-rendered surface as if it succeeded, which is worse than a hard throw. The non-strict fallback path is a separate feature (caller-side stale-canvas-retain plus orchestrator try/catch classification) outside the cutover scope. pnpm typecheck / lint / vitest 638/638 clean.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0855f76201
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| // Run develop with delta-applied state | ||
| const deltaState = applyDevelopDelta(state, region.adjustments); | ||
| const localBuild = buildDevelopPasses(device, caches, deltaState, outputW, outputH, INTERNAL_FORMAT, false); |
There was a problem hiding this comment.
Preserve geometry transform in local adjustment renders
在 composeLocalAdjustments 中这里调用 buildDevelopPasses(...) 没有传 geoOverride,会触发 buildDevelopPasses 的默认 createPassthroughGeometryUniforms 分支;但该链路输入仍是原始 srcInput。当存在裁剪、旋转、透视或镜像等非默认几何设置时,局部调整层会按未几何变换的源图重算,再与已几何变换的 baseTex 进行蒙版混合,导致局部调整区域错位/取样错误。
Useful? React with 👍 / 👎.
| ? loadLut3DTexture(device, resolvedProfile.customLut.path, resolvedProfile.customLut.size) | ||
| : Promise.resolve(caches.placeholder3D), | ||
| resolvedProfile.printLut | ||
| ? loadLut3DTexture(device, resolvedProfile.printLut.path) |
Summary
End-to-end migration of the image render pipeline from WebGL2 + twgl + GLSL to native WebGPU + WGSL, ending with full retirement of
src/lib/renderer/and thetwgl.jsdependency. Two long-running tasks closed in this PR:render-kernel-webgpu-rewrite(s0–s6) andlegacy-renderer-cutover(s1–s7).src/lib/gpu/covering develop (inputDecode / geometry / master / hsl / curve / detail / outputEncode), film (prep / colorLut / print / grain / effects), post (halation / glow / gaussianBlur), masking (linear/radial/brushStamp/rangeGate/maskInvert), carrier (halftone / ascii two-stage compute chain), signal damage (channelDrift), overlay (timestamp / normalLayerBlend), filter2d post-process. WGSL hand-written; no code-gen step.RenderSurfaceHandle;*IfSupportedfallback branches retired.canvasMaterializationsper render = 1.WebGPURenderBackendis the only backend wired intorenderSingleImageToCanvas;imageProcessing.tslegacyrenderWithPipeline/RenderManagerchain deleted.src/lib/renderer/removed entirely — every.frag/.vert/.glslsource, every twgl pipeline / texture pool / program registry. ~9000 LOC retired across the cutover slices.twgl.jsdependency dropped from package.json + lockfile.scripts/generate-shaders.ts(compiled the GLSLMasterAdjustment.frag/FilmSimulation.fragfor the dead WebGL2 pipeline) deleted along with itsdev/build:client/verify/ CI invocations. 8scripts/gpu-smoke/*.htmlharnesses +webgl2Reference.tsdeleted (one-off WebGL2 reference smoke tests for closed slices);ascii.html+passthrough.htmlretained.docs/decisions.md(Backend / ASCII / Brush mask / WGSL surface-adapter sections); bothdocs/tasks/*.{md,json}task pairs deleted per long-task closure rule.Test plan
pnpm typecheckclean (no WebGL2 types remaining;RenderImageStageDebugInfocarriesstatus+activePasses+boundaries)pnpm lintcleanpnpm vitest run638/638 pass (133 → 125 test files; the deleted 8 cover the deleted WebGL2 PipelineRenderer / ProgramRegistry / RenderManager / RenderPostProcessing / TiledRenderer / gpuSurfaceOperation / imageProcessing.debug paths)pnpm buildclean (vite 4.03s ✓, server 165ms ✓; full import graph resolves with no missing?rawmodules)pnpm dead-codereports no s7-introduced findingsrenderSingleImageToCanvas → WebGPURenderBackend → orchestrator → wgsl passeswas validated end-to-end through s2–s6 slices on prior commits.Slice closure trail (newest first)
6dda5ce→270ccd6— claim → dead-file purge → develop pipeline retirement → utility relocation →src/lib/renderer/deletion → docs closure → 2 review roundsc82fc9e→34926c2— ASCII carrier wiring (analysis cellColor + toneNormalize + composition full feature parity + asciiEffect switch + review findings)93a4e03→001624a— overlay timestamp + normalLayerBlend WGSLa0cd6f0→f727d72— effect mask shapes + range gate WGSL + revieweba7ef7→325c040— filter2d + masked blend WGSL + reviewaf69e70→d0a023b— channel drift WGSL + review363553d→6c518b2— halftone carrier WGSL + reviewbf5adcb→81d9435): WebGPU kernel s0–s6 (foundation, ascii compute, develop core / extended, film passes, masking & post, backend adapter, integration)