Files
Sanctification/card-harness/PERFORMANCE-PLAN.md

330 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.