Skip to content

Add configurable rendering budgets, compact diagnostics and safer presets - #1

Open
velrino wants to merge 7 commits into
achrefelouafi:mainfrom
velrino:perf/reduce-rendering-load
Open

Add configurable rendering budgets, compact diagnostics and safer presets#1
velrino wants to merge 7 commits into
achrefelouafi:mainfrom
velrino:perf/reduce-rendering-load

Conversation

@velrino

@velrino velrino commented Sep 9, 2026

Copy link
Copy Markdown

Hi @achrefelouafi! First of all, congratulations on this project — the procedural abilities look fantastic, and the care you put into the effects and the live VFX editor really shows. Thank you for sharing it!

While exploring the sandbox on my MacBook M4 Max, I noticed sustained rendering load and identified a few possible improvements. I wanted to contribute some options that could make the experience more comfortable on different devices while keeping the visual character of your work. These are suggestions, and I'd be happy to adjust the approach or narrow the scope based on what you feel fits the project.

TL;DR

Add configurable rendering budgets, Balanced/Economy profiles and a compact performance panel, reduce unnecessary rendering work, and improve preset validation and recovery. The latest commit includes 19 passing regression tests, a successful production build and a browser check of renderer initialization and adaptive resizing.

Live demo

@achrefelouafi, you can try the changes here: https://linear-ability-casting-threejs.vercel.app

This deployment contains commit 47fb55c, the current PR head.

🎥 Video

This recording demonstrates the performance panel from an earlier iteration of this branch; it predates the latest adaptive-resolution and preset-recovery changes.

record.mp4

Motivation

The original setup renders at display refresh rate, with a pixel-ratio cap of 1.75 and 4096² shadows refreshed every frame. On my Mac, this prompted me to explore adjustable quality settings and ways to avoid work when effects are inactive.

Changes

  • Use a Balanced budget of 60 FPS active / 30 FPS idle, a pixel-ratio cap of 1.25 and 2048² shadows refreshed at 30 Hz; suspend rendering in hidden tabs.
  • Hide inactive particle systems, skip unnecessary depth/distortion passes, and update sun shadows only in the main pass. Disable canvas MSAA and select PCFShadowMap directly.
  • Add an Economy profile with 30/15 FPS, DPR 1, 1024² shadows at 15 Hz, half-resolution bloom, adaptive resolution and a smaller effect-light pool. Bloom remains enabled while idle by default. Light-pool changes take effect after reload.
  • Suggest an initial profile using coarse device signals when no preference is stored, while preserving the user's saved choice.
  • Add a small top-center FPS indicator that expands into a 320 px popover with Metrics, Graphics and Compare tabs, limited scrolling and keyboard support. Labeled 10-second samples can be exported as JSON; GPU timing is optional where supported.
  • Keep device graphics preferences separate from artistic presets. Validate imports before mutation and preserve unreadable stored presets. Failed persistence leaves saved state intact, and a failed backup prevents overwriting the original collection.
  • Address timing and particle-visibility regressions introduced during this work, including 15 FPS timing, minimum particle lifetime and live lifetime editing after hiding. Initialize resolution scaling before the renderer's first sizing operation.

Validation

Latest commit (47fb55c):

  • npm test: 19 tests pass, covering timing, particle visibility, shadow cadence, adaptive resolution, profile selection, preset validation and persistence/recovery failures.
  • npm run build and git diff --check: pass.
  • Browser check with a real WebGLRenderer: valid initial DPR/canvas dimensions before settings synchronization, followed by adaptive resizing; no console errors observed.

Earlier commits also received browser smoke checks for all ten abilities, visibility suspension/resumption, performance-panel interactions, mobile layout and profile persistence. These were not all repeated after the latest changes and do not constitute full visual lifecycle verification.

Validation notes and measurement conditions · Latest test/build output and browser results

Tradeoffs and measurement limits

Earlier short idle comparisons recorded roughly 120 → 59–61 draw calls per frame, alongside a frame-rate change from about 120 to 30 FPS. Aggregate CPU work per second decreased, while GPU samples varied substantially. These observations do not establish a stable per-frame GPU speedup; power and temperature have not been measured.

Lower resolution, frame rate, bloom resolution and shadow refresh trade some sharpness and smoothness for less rendering work. Full render-on-demand was left out because the scene continues animating while idle. The controls make these tradeoffs adjustable, and I'd welcome your feedback on the defaults and overall scope.

Thank you again for making this project available — it has been a pleasure to explore, and I hope this contribution is useful!

@vitorbarbosa19

Copy link
Copy Markdown

@velrino

PR review — Linear Ability Casting Extended ThreeJS #1

HOLD · 2 blockers

PR #1 — configurable rendering budgets, diagnostics, and safer presets

Open the pull request · reviewed 2026-09-10

Exact head 47fb55c23879cbb1b35e4a33f98b8c254c3cafff
Base main@8c377c886ac2c68b26bc65c8f3507905818b974a
Size 30 files · +2,127 / −151
Merge state observed Mergeable / clean

TL;DR

The implementation is unusually thoughtful for a large first contribution: the frame budget, hidden-tab suspension, conditional passes, safer preset writes, tests, and documentation mostly agree with the PR claims. I would not merge this head yet because the new Compare export can silently aggregate or label metrics against rendering state that was not actually constant, and "Export all presets" drops the exact quarantined entries the recovery work promises to preserve.

The remaining two issues are smaller but real: wheel zoom does not wake Economy mode from 15 FPS, and the adaptive-resolution heuristic mistakes a genuinely 30 Hz display for a GPU overrun.

Findings

🔴 BLOCKER 1 — Compare samples do not bind to the effective rendering state

Evidence: PerformancePanel.js lines 198–235 stores configured settings plus CPU/call/GPU arrays, but not adaptive scale. The scale passed to record() is used only in the current canvas label; the completed sample/export contains no scale range or trajectory.

Economy enables adaptive resolution by default. A 10-second recording can therefore step through 1.0 → 0.85 → 0.7 → 0.6 while continuing to average metrics as if it were one controlled condition. The setting-comparison guard cannot detect this because renderer.resolutionScale is not part of settings.performance.

A second instance of the same integrity problem occurs after switching profiles: the browser probe observed configured lightCount: 4 but an effective six-light pool until reload. The JSON sample records four, even though six lights were actually evaluated. The UI note discloses the reload requirement, but the exported measurement does not.

Risk: the feature intended to compare performance can produce an apparently precise report that blends multiple pixel budgets or labels the wrong light budget.

Suggested fix: freeze adaptive resolution for a recording, cancel/restart when effective scale changes, or record per-frame scale and report its range/trajectory. Also include effective light-pool size or block recording until a required reload. Add tests that a completed sample cannot silently mix these states.

🔴 BLOCKER 2 — "Export all presets" omits quarantined presets

Evidence: failed stored entries are intentionally moved into this._quarantine at PresetManager.js lines 83–95. But exportAll() serializes only this.presets.

The focused probe loaded one valid and one stale preset. The manager reported one quarantined entry, while the exported JSON contained only the valid preset.

Risk: a user can choose "Export all presets" as a backup/migration step, clear the browser or move devices, and permanently lose the exact unreadable entry this PR says is being preserved. A wholly unreadable backup is even less discoverable: it is copied to a private storage key but has no visible export/recovery route.

Suggested fix: include { ..._quarantine, ...presets } in collection exports, and expose a raw download/recovery path for a wholesale unreadable blob. Add a regression test proving quarantined bytes survive export unchanged.

🟠 SHOULD FIX — Wheel zoom stays at the 15 FPS idle budget

Evidence: wheel zoom is handled directly inside CameraRig, while App only marks InputManager pointer/action events active.

Browser proof in Economy mode: target FPS was 15 before the wheel event; the desired camera distance changed from 11.5 to 12.97, but target FPS remained 15. A subsequent pointer-move immediately raised it to 30.

Impact: scroll zoom and its damping animation are visibly sampled at the idle rate, contradicting the claim that the first input snaps the loop active.

Suggested fix: give CameraRig an interaction callback (wheel and OrbitControls start/change) that calls App._markActive(), with a browser or unit regression.

🟠 SHOULD FIX — A real 30 Hz display is classified as an overrun

Evidence: AdaptiveResolution.sample() compares rendered intervals with a target capped at 60 FPS, but it does not know when the display itself is below 60 Hz. Feeding six seconds of perfect 30 Hz frames with a 60 FPS target produced [0.85, 0.7, 0.6]; the final scale was 0.6.

The existing "display slower than the cap" test covers a 120 FPS setting on a 60 Hz panel, not a 60 FPS setting on a 30 Hz panel.

Impact: a visible page on a 30 Hz display or comparable browser-level throttle loses 40% render scale without gaining a frame.

Suggested fix: establish a conservative effective refresh ceiling independent of the workload, or use CPU/GPU evidence to distinguish refresh-limited cadence from render overrun. Add the 30 Hz negative control.

Claims vs. repository vs. implementation

PR claim Repository / implementation read Verdict
Configurable Balanced/Economy budgets and hidden-tab suspension Implemented in settings, App loop, visibility handler, renderer and profile persistence. ✅ MATCH
Skip idle depth/distortion work and throttle shadows Conditional pass logic and independent cadence objects are coherent; shadow flags are preserved so only the main pass owns the sun update. ✅ MATCH
Compact diagnostics and comparable 10-second reports Panel works and exports, but does not bind samples to effective adaptive scale/light count. 🔴 DIVERGES
Validate imports and preserve unreadable presets Validation and write atomicity are strong, but the user-facing "Export all" path excludes quarantined data. 🔴 DIVERGES
19 regression tests, production build, renderer initialization check Independently reproduced at the exact head; committed author logs used Vite 8.2.2 while the lock pins 8.1.5, but the locked 8.1.5 build also passes. ✅ PROVEN

Local proof at exact head

  • npm ci — pass; package lock installed Vite 8.1.5.
  • npm test — 19/19 pass. Expected negative-path warnings were emitted.
  • npm run build — pass; 133 modules transformed.
  • git diff --check origin/main...HEAD — pass.
  • Real headless Chromium/WebGL renderer initialization — pass; initial DPR 1.25 and adaptive DPR 0.75.
  • Balanced and Economy profile selection/persistence — pass.
  • Mobile 390×844 panel bounds — pass; 320 px panel stayed inside viewport.
  • All ten abilities instantiated and advanced under the PR renderer with no console/page errors.
  • Local route responses — no 4xx/5xx observed in the exercised flow.

Environment note: headless SwiftShader was much slower than physical GPU hardware, so its FPS number is not treated as a performance benchmark. It was used only for functional rendering, interaction, initialization, and error checks.

Rendered receipts

Open the contributor's live demo (public deployment linked by the PR).

  • linear-ability-pr1-main.png — Main sandbox render at the exact local PR head.
  • linear-ability-pr1-performance-panel.png — Graphics tab after selecting Economy; configured values persisted correctly.

(Both in /tmp/design-renders/.)

Decision

Recommendation: request changes. Fix the two data-integrity blockers, add focused regressions, and then re-check the two input/refresh-rate advisories. The rendering optimizations themselves are directionally good and the branch is close, but the PR should not advertise trustworthy comparison and preset recovery while those two outward-facing paths can misstate or omit data.

Founder calls: none. The PR asks generally for feedback but presents no explicit unresolved choice that must be decided before the author can fix these defects.

No review, approval, merge, or comment was posted to the external repository. This is a read-only review artifact.

@velrino

velrino commented Sep 10, 2026

Copy link
Copy Markdown
Author

Thank you, @vitorbarbosa19, for the thorough review and the concrete reproductions. The goal is to improve what we have, and this feedback helped identify gaps in both the comparison reports and preset recovery.

I've addressed all four findings in 8defa2d, now pushed to this PR:

  • Comparison integrity: completed samples include an effective-state timeline with frame/time offsets, adaptive scale, canvas dimensions, and actual light-pool size. Reports and the completion message flag mixed rendering states or a configured light budget awaiting reload. Scale changes take effect after recording the frame just rendered, so the reported state matches that canvas.
  • Preset recovery: “Export all presets” includes quarantined entries, preserving their JSON values. A new “Download unreadable backup” action exports a wholly unreadable collection verbatim, including after reload or when the storage backup failed. Recovery files may still need repair before import.
  • Wheel/orbit interaction: CameraRig now signals wheel and OrbitControls start/change events to the application's active-frame budget.
  • 30 Hz displays: reducing resolution now requires measured CPU or GPU workload evidence alongside slow frame cadence. A lightly loaded 30 Hz display no longer loses resolution just for missing a 60 FPS target. Without GPU timer support, the heuristic conservatively relies on CPU timings.

Validation after a fresh npm ci: 25/25 tests pass, npm run build passes with the locked Vite 8.1.5, and git diff --check passes. The added regressions cover mixed and constant comparison states, effective light counts, quarantine/raw-backup exports, wheel interaction, and the slow-display versus busy-GPU cases.

Thanks again for helping make this more reliable. The updated head is ready for another look.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants