330 lines
22 KiB
Markdown
330 lines
22 KiB
Markdown
# Card harness performance improvement outline
|
||
|
||
## Current visual-quality constraint
|
||
|
||
Fast/Balanced canvas scaling has been removed after user review: reduced artwork
|
||
and text clarity is unacceptable for the harness. Inspect, Lab, and Pack use full
|
||
device pixel ratio up to the established 2× cap. Lower-resolution card assets remain
|
||
a deferred option for later work. Earlier resolution experiments below are history,
|
||
not current controls or recommended implementation steps.
|
||
|
||
Prioritize immutable texture sharing with safe ownership/eviction, reserve-stack
|
||
batching where appearance and reveal order can be preserved, shader preparation,
|
||
and measured elimination of redundant shader work. Keep the approved artwork,
|
||
material treatment, antialiasing, and full drawing resolution as the quality target.
|
||
|
||
This outline began with the harness source and checks run on September 12,
|
||
2026. It proposes work; it does not claim measured frame-rate gains. The existing
|
||
README describes an older authored three-card pack; current `packSize` is 10 and
|
||
`rollPackContents` randomizes fixture, finish, and substrate.
|
||
|
||
## Initial source findings (before the first implementation pass)
|
||
|
||
- Inspect and Lab use one card renderer and material. They load the selected
|
||
artwork, finish mask, and optional text mask; fixture changes dispose old maps.
|
||
- Pack loads only its selected unique fixtures, sharing maps between repeated
|
||
fixtures within that pack. It can select up to ten unique artworks. Inspect and
|
||
Pack currently load separate texture objects even when they use the same fixture.
|
||
- All ten pack cards have individual card geometry and edge/front materials.
|
||
Unrevealed fronts are hidden, but remaining backs and edges stay visible.
|
||
- Pack retains cards and six 1024×1400 wrapper textures when switching away.
|
||
Invisible roots do not draw, but retained GPU allocations can matter on devices
|
||
with limited memory. Texture counts alone cannot prove a memory bottleneck.
|
||
- Wrapper deformation is already dirty-driven and batched once per frame. Mint
|
||
cards already skip wear calculations. GPU preparation already warms hidden
|
||
fronts and both edge recipes. Preserve these existing optimizations.
|
||
- Every animation callback redraws the scene, including settled idle poses.
|
||
It also reads canvas dimensions every frame. This is unnecessary work while idle;
|
||
whether layout reads are expensive during motion requires a browser trace.
|
||
- Resolution is capped at device pixel ratio 2 by default, with optional 1.5 and 1
|
||
caps. Texture anisotropy uses the device maximum. Metal performs extra artwork
|
||
samples for parallax; several substrates evaluate procedural relief twice.
|
||
- More catalog files affect catalog validation/loading, and can increase the
|
||
number of unique fixtures selected for a pack. They are not all uploaded just
|
||
because they appear in the selector.
|
||
|
||
## Implementation order
|
||
|
||
| Priority | Work | Intended benefit | Verification |
|
||
| --- | --- | --- | --- |
|
||
| 0 | Repair browser regression fixtures and cover the current ten-card pack | Establish trustworthy checks before optimization | Remove obsolete `packContents` import, pass explicit contents and a texture Map, and exercise every card through completion |
|
||
| 1 | Add reproducible per-mode measurement runs with fixed pack contents and separate phase samples | Identify CPU work, GPU work, preparation stalls, and retained-resource effects | Export environment, settings, median/p95, long frames, resource counts, and phase durations |
|
||
| 2 | Render on changes and throughout active motion/damping; stop drawing settled scenes | Reduce idle GPU use and heat in every mode | Confirm idle draw count stops, first input paints promptly, and all motion settles correctly |
|
||
| 3 | Move resize checks to resize notifications with device-pixel-ratio change handling | Avoid repeated layout reads | Resize container, collapse mobile toolbar, rotate device, change resolution, and move between displays |
|
||
| 4 | Share immutable fixture textures across modes with reference-counted ownership and bounded eviction | Reduce duplicate uploads, memory, and fixture-switch stalls | Fixture switches, rerolls, refresh revisions, failed loads, canceled requests, and local image replacement release only unused resources |
|
||
| 5 | Share identical geometry and profile reserve-stack draws | Reduce pack allocation and possible draw overhead | Preserve full ten-card stack, extraction, edge appearance, reveal ordering, and inspection clearance |
|
||
| 6 | Profile texture sampling and shader preparation at full resolution | Identify redundant sampling and first-use stalls | Preserve sharpness, microdetail, and protected text in front/grazing and moving-light reviews |
|
||
| 7 | Optimize measured expensive shader paths | Improve moving-card frame time without changing approved materials | Pixel regression plus real-device grazing-angle and moving-light review |
|
||
| 8 | Load Pack code and optional wrapper styles on demand, only if loading measurements justify it | Reduce startup download and preparation cost | Pack retry, style-switch latency, and program reuse after preparation |
|
||
|
||
First repair the browser check, then implement measurement, demand rendering, and resize handling. Texture sharing
|
||
is the next independent improvement. Change shader math or stack rendering only
|
||
when measurements identify those costs. Code splitting primarily helps startup;
|
||
it does not by itself improve an already loaded scene's animation frame rate.
|
||
|
||
For demand rendering, explicitly invalidate on artwork completion, UI material/light
|
||
changes, camera movement, pack preparation/state changes, resize, and capture.
|
||
Keep frames running during rotation damping, OrbitControls damping, sweeps, touch
|
||
settling, opening, lift, reveal, and advance. Pause/resume and hidden-tab behavior
|
||
must preserve elapsed progress. Idle gaps must not enter the animation frame-time
|
||
statistics as slow frames. Lab controller displays and capture must still update.
|
||
|
||
For shader work, begin with operations whose contribution is zero, such as the
|
||
normal-map sample when its substrate scale is zero. Keep optional masks, filtered
|
||
microdetail, anchored ink, and relief equations intact. Evaluate shader
|
||
specialization only if traces justify the added variants and preparation cost.
|
||
|
||
## Benchmark matrix and acceptance
|
||
|
||
Use a production build on the affected device and one mobile device. Record browser,
|
||
GPU, refresh rate, viewport, drawing buffer, pixel ratio, anisotropy, fixture,
|
||
finish, substrate, lighting, and cold/warm state. Use fixed contents for pack
|
||
comparisons, including repeated fixtures and ten distinct fixtures where available.
|
||
|
||
- Inspect/Lab: idle, drag, sweep, and front/grazing/back; Paper, Linen, Metal, Wood,
|
||
and Leather; Printed ink, Foil, and Holographic; mask absent/present; mint/worn.
|
||
- Pack: first preparation, tear/dent, extraction, face-down stack, lift, reveal,
|
||
inspection drag, advance, restart, reroll, and leave/return.
|
||
- Resources: repeat fixture switching and rerolls; compare Inspect before and
|
||
after preparing Pack. Counts should plateau for a fixed workload; compare against
|
||
expected retained allocations rather than expecting all counts to return to zero.
|
||
|
||
Collect at least ten seconds of active motion per representative case after warmup,
|
||
with at least three repetitions. Keep cold preparation separate. Record CPU update
|
||
and render submission time separately from GPU time; use asynchronous GPU queries
|
||
only when supported, rejecting disjoint results. Do not introduce synchronous GPU
|
||
readback into the timed path. Report slow frames per phase rather than averaging
|
||
opening and idle periods together.
|
||
|
||
For a 60 Hz target, the frame budget is approximately 16.7 ms. An initial proposed
|
||
acceptance criterion is p95 active-motion frame intervals no worse than 20 ms on
|
||
agreed target devices, with fewer long frames and no visual/state regressions.
|
||
This is a proposed target, not a demonstrated result. Idle should produce no
|
||
repeated scene draws after settling. Compare battery/heat behavior separately.
|
||
|
||
## Check results for this outline
|
||
|
||
- `npm test`: all 31 current catalog, texture, material, geometry, pack state,
|
||
batching, GPU preparation, and cleanup checks passed.
|
||
- `npm run build`: TypeScript and Vite production build passed. Vite reports a
|
||
roughly 700 kB main JavaScript chunk; this is a loading observation, not an FPS
|
||
measurement.
|
||
- Fresh `scanCardCatalog()`: nine valid fixtures, three with optional text masks,
|
||
zero errors, approximately 1795 ms for a local scan. In development, the Vite
|
||
catalog endpoint performs this scan per request; it is not per-frame work.
|
||
Cache validated entries by file change if catalog latency becomes a concern.
|
||
- Browser check attempted with local `/usr/bin/firefox` and a 60-second timeout.
|
||
It failed by timeout; Firefox also reported Snap mount warnings. No shader pixel
|
||
or pack GPU result was returned. This is not evidence of pixel equivalence.
|
||
- Source review independently found a broken browser test integration:
|
||
`scripts/shader-pack-preparation.mjs` imports removed `packContents`, omits the
|
||
now-required `contents` option, passes a plain texture object instead of a Map,
|
||
and limits the action cycle to the old three-card workflow. Its top-level import
|
||
is used by `shader-pixel-client.mjs`, so the obsolete export can prevent the
|
||
combined check from running even with a working browser. Repair this first.
|
||
- Most Node pack state checks retain the three-card fixture; they pass but do not
|
||
establish full ten-card rollout/advance coverage. Add deterministic ten-card
|
||
coverage and random-pool/pack-size checks as part of repairing the check suite.
|
||
- No interactive browser smoke test or physical-device CPU/GPU benchmark passed
|
||
in this environment. Software WebGL, when available, can validate correctness
|
||
and program reuse; physical-device frame-rate improvements require separate
|
||
measurement.
|
||
|
||
|
||
## First implementation pass (historical)
|
||
|
||
The reported application baseline is 1852×920 pixels, median 50 ms (approximately
|
||
20 FPS), p95 83 ms, worst 267 ms, 22 draws, 28 textures, and 8 programs. This is
|
||
an observation from the user's screenshot, not a locally reproduced benchmark;
|
||
the screenshot does not identify the pack phase, hardware, or resolution setting.
|
||
|
||
Implemented:
|
||
|
||
- Settled Inspect, Lab, and Pack poses stop issuing scene draws. Animation
|
||
callbacks remain available; active motion, input, camera damping, touch settling,
|
||
and asynchronous artwork/material/state changes request draws. Idle frame gaps
|
||
are excluded from sampled consecutive-draw intervals and the HUD labels idle.
|
||
- Canvas sizing uses ResizeObserver and window notifications. Per-frame work only
|
||
checks device pixel ratio for display changes, without reading canvas layout.
|
||
- Ten pack cards now share two geometry objects (face and edge), replacing thirty
|
||
separate objects. Individual transforms/materials and draw calls are retained.
|
||
Sharing reduces preparation/allocation costs; it does not remove stack draws.
|
||
- The HUD reports median CPU scene-update and render-submission time separately.
|
||
Submission can include driver waits and is not asynchronous GPU timing. Browser
|
||
work and input handlers outside the animation callback are not included.
|
||
- Browser preparation checks now use deterministic current-size rolled contents,
|
||
an explicit texture Map, and enough transitions to reveal every selected card.
|
||
- Added Node checks for ten ordered reveals, completion, geometry ownership,
|
||
paused/hidden/settled pack frames, and sampling across idle/visibility gaps.
|
||
|
||
Validation after this pass: all 36 Node checks pass; the production build passes.
|
||
Using the installed Firefox executable directly avoids the former Snap launcher
|
||
failure and the repaired browser module now reports an explicit WebGL-context
|
||
creation failure. Pixel parity and interactive browser behavior remain unverified
|
||
here. No approved shader formulas, default resolution, anisotropy, materials,
|
||
wrapper geometry, or ten-card stack appearance were intentionally changed.
|
||
|
||
Next compare the same active pack phase after reload, at Full and Fast, recording
|
||
CPU update/submit alongside frame intervals. Idle suppression should reduce idle
|
||
GPU activity and heat; this pass does not establish improved active-motion FPS.
|
||
If scene update remains low while frame intervals stay high, investigate GPU,
|
||
driver, and browser costs. Prioritize shared textures and measured stack draw cost
|
||
before changing shader appearance.
|
||
|
||
|
||
## Second implementation pass: effective comparisons and renderer costs (historical)
|
||
|
||
The new Full/Fast screenshots both show 1852×920, approximately 33 ms median
|
||
intervals, four draws, eight textures, and six programs in Inspect. They are not
|
||
a lower-resolution GPU comparison: at DPR 1, the old ratio caps both selected 1.
|
||
They also label last-motion timings while idle. Tiny CPU scene-update/submission
|
||
medians do not prove fast GPU execution, nor do they rule out browser pacing,
|
||
compositing, or CPU work outside the animation callback.
|
||
|
||
Implemented:
|
||
|
||
- Full stays unchanged; Balanced is 75% and Fast 50% of Full's buffer dimensions.
|
||
Fast now reduces pixels by approximately 75% even at DPR 1.
|
||
- Both artwork faces are outward-facing, single-sided surfaces. Culling inward
|
||
triangles avoids processing an opaque face from the wrong side. Wrapper surfaces
|
||
remain double-sided so curls and an open pouch remain visible.
|
||
- Removed CSS backdrop blur from overlays above the animated canvas to reduce
|
||
repeated compositor work. Material appearance is unchanged by this UI change.
|
||
- Removed unconditional edge `needsUpdate`. Color/roughness/metalness update as
|
||
uniforms; the installed MeshPhysicalMaterial's clearcoat setter already increments
|
||
the version when its shader feature switches between zero and positive.
|
||
- Added separate idle/drawing callback medians and finer CPU timing display.
|
||
- Added sampled asynchronous WebGL2 elapsed-time queries when supported, with a
|
||
five-per-second sample rate, maximum eight pending queries, delayed availability
|
||
checks, and disjoint-result rejection. No synchronous readback or GPU waits.
|
||
- Added reported GPU identity, DPR, resolution scale, and MSAA sample count. Identity
|
||
can be generic when browser privacy settings restrict the debug extension.
|
||
|
||
Interpretation after reload: verify Fast's buffer dimensions actually shrink.
|
||
If idle callback median is already about 33 ms, investigate browser/display pacing
|
||
and embedded-preview behavior; scene drawing is stopped during those idle samples.
|
||
If idle is approximately 16.7 ms and drawing is slower, use GPU elapsed time to
|
||
separate render cost from driver/presentation/compositing overhead. GPU queries
|
||
measure issued rendering work, not the complete browser presentation path.
|
||
|
||
Inspect has front, back, edge, and floor draws, with no enabled shadow pass or
|
||
postprocessing. Instancing the reserve backs/neutral edges could reduce Pack draws;
|
||
it will not resolve a four-draw Inspect workload by itself. Texture sharing remains
|
||
useful for Pack memory and preparation. Defer changes to approved shader formulas
|
||
until a valid buffer-size comparison and GPU measurements justify them.
|
||
|
||
References: [Three.js material sides](https://threejs.org/docs/pages/Material.html),
|
||
[renderer options](https://threejs.org/docs/pages/WebGLRenderer.html), and
|
||
[Khronos asynchronous WebGL2 timer-query specification](https://registry.khronos.org/webgl/extensions/EXT_disjoint_timer_query_webgl2/).
|
||
|
||
|
||
Validation after this pass: all 43 Node regression checks and the production build
|
||
pass. New checks cover DPR-1 pixel reduction, callback sample separation, query
|
||
availability before result retrieval, disjoint rejection, query limits/cleanup,
|
||
material version reuse, and outward face orientation. Local WebGL pixel and
|
||
interactive verification remains unavailable because Firefox cannot create a
|
||
WebGL context here. No active-motion frame-rate gain is claimed until the affected
|
||
application is measured after reload.
|
||
|
||
|
||
## Removal of canvas quality modes
|
||
|
||
Removed the toolbar selector, resolution-change handler, diagnostic scale label,
|
||
and scale argument from the pixel-ratio helper. Full-resolution rendering and the
|
||
CPU/GPU/pacing diagnostics remain. The existing resolution regression check now
|
||
verifies the full device pixel ratio with the established 2× cap. Compare measured
|
||
CPU/GPU costs at full quality for subsequent optimization; do not ask reviewers to
|
||
accept lower canvas resolution as the performance fix.
|
||
|
||
|
||
## Exportable logs for the 33 ms investigation
|
||
|
||
Implemented opt-in 30-second recordings with toolbar start/stop and JSON export.
|
||
Logs separate browser callback intervals/actual arrivals from draw counts, CPU
|
||
update/submission/whole-callback work, and asynchronous GPU elapsed time. Summaries
|
||
include cadence buckets, idle/drawing comparisons, and per-mode/phase work. Context
|
||
metadata includes browser, embedded status, focus/visibility, GPU, MSAA, full
|
||
pixel ratio/buffer, Three.js revision, and supported performance observers.
|
||
|
||
Input/state/focus/visibility/context events and supported long-task/long-animation-
|
||
frame observations supply context outside the renderer. Visibility gaps break
|
||
continuity without discarding genuine stalls. Delayed GPU results reference the
|
||
frame that issued the query. Samples are bounded and exported after recording;
|
||
per-frame console output and synchronous GPU waits are avoided.
|
||
|
||
First capture five seconds of idle, sweep/drag/flip, and idle again in Inspect.
|
||
Record Pack separately. Read the resulting JSON to decide whether 33 ms exists
|
||
without scene draws, appears under GPU rendering load, or coincides with outside-
|
||
callback work. Logs do not directly expose display refresh or browser power policy
|
||
and do not guarantee one definitive cause.
|
||
|
||
|
||
Recorder validation: all 48 harness Node checks and the production build pass.
|
||
Focused checks also verify GPU result timestamps and phase summaries. Browser
|
||
recording/diagnosis still requires a capture from the affected application; local
|
||
Firefox's previously confirmed WebGL-context limitation remains.
|
||
|
||
|
||
## First application capture: software-rendering lead
|
||
|
||
Preserved [the supplied Firefox Inspect log](benchmarks/card-harness-timings-2026-09-13T03-16-37-992Z.json)
|
||
outside `dist` so a later production build will not delete it. This capture lasts
|
||
13.413 seconds and covers idle, sweep, flip/settling, and drag on Linux Firefox 155
|
||
in a visible, focused normal tab, at DPR 1, development mode, and 4× MSAA.
|
||
|
||
Findings:
|
||
|
||
- Callbacks after idle: median 17 ms, p95 33 ms. Callbacks after drawing: median
|
||
33 ms, p95 50 ms, worst 67 ms. This is not a global fixed 30 Hz callback cap.
|
||
- The five-second sweep produces 151 draws, approximately 30 draws per second.
|
||
- Measured whole animation-callback work: p95 1 ms, worst 2 ms. CPU submit:
|
||
p95 1 ms, worst 2 ms. Browser times are quantized to whole milliseconds in this
|
||
capture; zero means below available measurement resolution, not no work.
|
||
- Eight textures and six programs remain stable. Rendered frames use four draws
|
||
and 270 triangles. This capture shows no growing resource-count trend; it does
|
||
not evaluate Pack or establish absence of leaks in other workloads.
|
||
- GPU elapsed timing and long-task/long-animation-frame observers are unavailable,
|
||
so their absence is not evidence that graphics/browser work is fast.
|
||
- Reported renderer: `llvmpipe, or similar`. Mesa documents LLVMpipe as a
|
||
multithreaded software rasterizer. This is a strong software-rendering lead,
|
||
not a direct GPU timing measurement or complete driver diagnosis. Software
|
||
rasterization can execute outside the measured JavaScript submission callback.
|
||
|
||
Next inspect Firefox `about:support` → Graphics, particularly WebGL 2 driver
|
||
renderer, compositing, and graphics decision/failure information. Confirm whether
|
||
hardware acceleration is active in this browser session and investigate driver,
|
||
virtual/remote-session, or browser-policy fallback where indicated. Compare a
|
||
capture from a browser/session confirmed to use the actual hardware GPU, keeping
|
||
full-quality artwork and viewport settings. Defer further visual shader tuning
|
||
until the rendering environment is established.
|
||
|
||
Sources: [Mesa LLVMpipe documentation](https://docs.mesa3d.org/drivers/llvmpipe.html),
|
||
[Firefox Graphics troubleshooting](https://support.mozilla.org/en-US/kb/use-troubleshooting-information-page-fix-firefox),
|
||
and [Firefox hardware acceleration guidance](https://support.mozilla.org/en-US/kb/upgrade-graphics-drivers-use-hardware-acceleration).
|
||
|
||
|
||
## Firefox Graphics report confirms software rendering
|
||
|
||
Preserved [the supplied Graphics report](benchmarks/firefox-graphics-2026-09-13.txt).
|
||
Both WebGL driver renderer entries explicitly name `Mesa -- llvmpipe (LLVM 21.1.8,
|
||
256 bits)`. Compositing is `WebRender (Software)`; the active adapter is
|
||
`mesa/llvmpipe`, and the detected NVIDIA adapter (vendor `0x10de`, device `0x2208`)
|
||
is inactive. Target frame rate is 60 on a KDE Wayland desktop.
|
||
|
||
The decision log marks hardware compositing blocked by platform and includes
|
||
`FEATURE_FAILURE_SOFTWARE_GL` and `FEATURE_FAILURE_NO_DRM_DEVICE`. This confirms
|
||
software rendering; it does not establish the exact cause of hardware GPU access
|
||
or graphics-driver initialization failure. Browser-package isolation, graphics
|
||
runtime/driver mismatch, and device access need to be checked on that desktop.
|
||
|
||
The workspace's NVIDIA device is an RTX 2060 (`0x1f03`), different from the supplied
|
||
browser report's device. No Firefox process was found locally. Consequently local
|
||
workspace GPU permissions/installed Snap information cannot diagnose the user's
|
||
browser session and should not be used to select a desktop fix.
|
||
|
||
Next obtain the browser machine's Linux distribution and Firefox installation
|
||
method (distribution package, Flatpak, Snap, or another build). Then check the host
|
||
hardware driver and the applicable browser runtime/device access. Do not bypass
|
||
Firefox graphics blocks or alter full-quality harness rendering to compensate for
|
||
this fallback. Capture again once WebGL 2 reports the hardware NVIDIA renderer.
|