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

22 KiB
Raw Blame History

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, renderer options, and Khronos asynchronous WebGL2 timer-query specification.

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 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, Firefox Graphics troubleshooting, and Firefox hardware acceleration guidance.

Firefox Graphics report confirms software rendering

Preserved the supplied Graphics report. 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.