31 Commits

Author SHA1 Message Date
xfy
334e65fc76 test(physics): mirror Kerr RK45 loop in CPU, cover spin=0 degeneracy + spin>0 capture
Phase 2 shipped the Kerr deriv() and the adaptive RK45 integration loop in
the shader (black_hole.wgsl:260, 320-390), but the CPU mirror in physics.rs
only covered the single-step Kerr derivative (kerr_bending_accel), not the
loop. That left the AGENTS.md 'CPU <-> shader mirror' contract broken for
Phase 2: the spin-dependent capture radius, the adaptive step control, and
the budget = accepted-steps-only / dt_min forced-accept semantics existed
only on the GPU, with nothing testable on the CPU side.

Add a faithful CPU mirror and the loop-level tests that close the gap:

- rk45_step(): Dormand-Prince step mirroring black_hole.wgsl:260-285, using
  kerr_bending_accel so it is also the Phase 1 step at chi=0.
- is_captured_rk45(): the full adaptive loop mirroring black_hole.wgsl:320-390
  — same seeding (total_path / steps), same reject/retry at dt_min (the
  forced-accept floor that prevents infinite retry), same accept-then-refine,
  and the spin-dependent capture radius r+(chi) via kerr_horizon.

Tests (tests/physics_test.rs):
- spin=0 RK45 loop captures below bcrit and escapes above (degeneracy).
- spin=0.9 still captures a b~2.0 ray (horizon shrinks but not past b<bcrit).
- capture set does not grow as spin increases across a bcrit-straddling sweep.
- rk45_step error shrinks monotonically with dt (the property the adaptive
  loop's reject/accept decision depends on).

Note: probing the error scaling showed it falls between 2nd and 4th order in
dt, not a clean 5th, because of the per-stage normalize() projection the
shader (and now the mirror) applies. The loop only relies on monotonicity, so
this is correct-as-shipped; documented in a doc-comment on rk45_step so a
future reader is not misled by the '45' in the name.

cargo test: 17 passed (3 Phase 1 inline + 14 integration, up from 12).
2026-07-14 15:51:53 +08:00
xfy
9afab04308 render: resolve clippy warnings in BlackHolePlugin systems
Fixes the following clippy warnings in plugin.rs:
1. clippy::too_many_arguments: Suppressed on spawn_fullscreen_quad since Bevy system functions naturally take many arguments as resources/queries.
2. clippy::field_reassign_with_default: Refactored instantiation of BlackHoleMaterial to set fields in the struct literal instead of post-creation assignment.
3. clippy::type_complexity: Suppressed on resize_offscreen and nudge_camera systems as Bevy queries with Filters/Disjunctions are inherently complex.
2026-07-14 15:09:39 +08:00
xfy
17fbbf98e1 render: derive Default for BlackHoleMaterial
Clippy warned that the manual Default implementation for BlackHoleMaterial
could be derived (clippy::derivable_impls). We now derive Default directly
on the struct and remove the manual implementation block.
2026-07-14 15:09:38 +08:00
xfy
699a8c974e ui: use std::f32::consts::PI instead of approximate 3.14 literal
Fixes clippy::approx_constant errors caused by using the hardcoded value 3.14
for camera yaw and accretion disk tilt sliders. Using std::f32::consts::PI
resolves the errors and uses the precise constant value.
2026-07-14 15:09:36 +08:00
xfy
c3ca1e002a physics: truncate BCRIT constant float precision
Clippy warned about excessive precision in the BCRIT float literal (2.5980762).
Since f32 has 24 bits of mantissa (approx 7 decimal digits of precision), the
excessive decimal digits are truncated to 2.598_076 to compile cleanly under
clippy::excessive_precision.

The integration test still passes as the difference is well within the 1e-5 limit.
2026-07-14 15:09:34 +08:00
xfy
22489f50da fix: group conflicting Transform queries in ParamSet to fix B0001 panic
resize_offscreen declared two Query<&mut Transform> params (FullscreenQuad
and UpscaleQuad filters). Bevy's conflict checker does not treat With<T>
filters as disjoint access — both queries declare write access to the same
component, so the app panicked at startup with error[B0001].

Merge the two queries into a ParamSet (borrowed one at a time) and move
them to the end of the parameter list per the Params convention.
2026-07-14 15:00:05 +08:00
xfy
ff7bf82f02 chore: silence dead-code warnings in physics test-mirror module 2026-07-14 14:40:46 +08:00
xfy
7cb5c2880a feat: add Spin slider + ISCO/Horizon read-only labels 2026-07-14 14:37:10 +08:00
xfy
d8b77c00c8 feat: Kerr deriv() with frame-dragging + spin-dependent horizon 2026-07-14 14:22:37 +08:00
xfy
7d4a53358f feat: derive disk_inner from Kerr ISCO 2026-07-14 14:18:36 +08:00
xfy
1bde9f382c feat: add kerr_isco and kerr_horizon CPU helpers with tests 2026-07-14 14:15:25 +08:00
xfy
61edcfabd2 feat: plumb spin parameter into GPU uniform 2026-07-14 14:12:31 +08:00
xfy
e158c8c668 feat: wire render_scale via offscreen render-to-texture + upscale 2026-07-14 14:09:39 +08:00
xfy
80c2b5dbe2 fix(camera): adjust default pitch and invert zoom scroll direction
- Set default pitch to 0.7 rad to keep the camera away from the π/2 gimbal pole, preventing horizontal drag from degenerating into an in-plane roll.
- Adjust scroll zoom logic to divide instead of multiply by the zoom factor, so that scrolling up zooms in (reduces distance) rather than out.
2026-07-13 18:11:04 +08:00
xfy
f80a24d191 fix: invert orbit camera yaw/pitch to view-follows-cursor direction 2026-07-13 17:38:02 +08:00
xfy
4556376b4a fix: make the black hole actually render (was stuck on grey clear color)
The app showed only the camera clear color (grey srgb 43,44,47) — the
fullscreen quad's fragment shader produced no output. Two issues; the
second was the real blocker.

1. render/material.rs: skybox.wgsl declared binding 1 as texture_cube<f32>
   but the material's #[texture(1)] defaulted to D2. The mismatched
   bind-group layout vs. shader caused the pipeline to fail to specialize.
   Fix: dimension = "cube" on the texture attribute so the layout matches.

2. SHADER COMPOSITION (the actual blocker): the shader was split across
   naga_oil modules (ray_gen, geodesic, stars, disk, planets, grid, skybox)
   pulled in via #import singularity::... This compiled and validated with
   zero errors, but at runtime calling ANY cross-module-imported function
   made the fragment output nothing (only the clear color showed). Local
   functions were fine. Confirmed by bisecting a minimal repro with the
   user watching the screen: local fn -> renders; imported fn (even pure
   math) -> grey.

   Rather than chase the naga_oil 0.22 / Bevy 0.19 composition bug, inline
   every function into black_hole.wgsl and drop the #import singularity::*
   lines. The 7 standalone module files are removed. Rendering now works.

Also fixes scene/planets.rs: upload_planets was allocating a fresh
ShaderBuffer (new handle) every frame, which re-triggered the
AsBindGroup RetryNextUpdate that the previous commit's startup pre-fill
was meant to eliminate. Now mutates the existing buffer asset in place
via set_data, keeping the handle stable.
2026-07-13 16:43:01 +08:00
xfy
ee15ddecb7 fix: make the renderer actually draw (step size, storage buffer, quad fit)
The committed Phase 1 code rendered a black screen due to three
independent bugs, fixed here:

- black_hole.wgsl: dt was sized as |eye|/steps, so the full integration
  only traveled |eye| units total — rays never reached capture or escape
  and every pixel fell through to accum=black. Now dt covers
  eye_dist + escape_r across the configured step count, and the escape
  test uses the dynamic escape_r instead of a hardcoded 1000.0.
- render/plugin.rs: the planets binding was Handle::default(), causing
  AsBindGroup to return RetryNextUpdate every frame and silently skip
  the fullscreen quad's draw. Pre-fill a MAX_PLANETS-sized zeroed
  ShaderBuffer at startup so the binding resolves immediately.
- render/plugin.rs: scale the fullscreen quad by half the window size
  to match Camera2d's default WindowSize projection (1 unit = 1 px);
  the old aspect-based scaling letterboxed the image.
- render/plugin.rs: add nudge_camera to work around Bevy 0.19 #24448,
  where a static camera stops rendering after the first frame.
- planets.wgsl: rotate world-space planet centers into disk-local space
  before the ray-sphere test (ray and centers were in different spaces).
2026-07-13 11:47:31 +08:00
xfy
87b089eb35 docs: README + silence reserved-field dead-code warning
README documents the renderer, run instructions (desktop + web), controls,
how the geodesic pipeline works, performance notes, and Phase 2 (Kerr) as
future work. Allow dead_code on BlackHoleParams since render_scale + spin
are intentionally reserved for Phase 2.
2026-07-13 10:04:44 +08:00
xfy
76d302b4e4 feat: web release build + web-tuned defaults verified 2026-07-13 10:03:01 +08:00
xfy
1e8cd93bc3 fix: keep full-screen quad filled on window resize
The quad mesh was built once at startup with the initial aspect ratio, so
widening the window afterward left empty edges. Add a FullscreenQuad marker
component and a fit_quad_to_window system that rescales the quad's Transform
to the live aspect each frame (only mutating on change).

Also remove the misleading 'Render scale' slider — it isn't wired to a real
sub-resolution render target in Phase 1 (the full-screen quad always renders
at window resolution). Lowering Steps is the real perf lever. Documented in
a code comment for future work.
2026-07-13 10:00:42 +08:00
xfy
7e0ac65d55 feat: full egui control panel + pointer routing 2026-07-13 09:57:54 +08:00
xfy
8931904495 feat: lensed planet ray-sphere intersection in geodesic tracer
Add planets as ray-traced spheres inside the RK4 geodesic loop. A new
planets.wgsl shader tests each integrator segment against a storage
buffer of SphereData (center/radius/color/emissive) and composites hits
front-to-back alongside the accretion disk. Lambert shading with a fixed
light direction; emissive flag bypasses shading.

- assets/shaders/planets.wgsl: segment-sphere intersection + shading
- src/scene/: Planet component, upload_planets system, default planet
- black_hole.wgsl: wire planet_hit into the compositing loop
- geodesic_schwarzschild.wgsl: replace tuple return with Deriv struct
2026-07-10 17:06:19 +08:00
xfy
983413e9af feat: egui UI wired with live disk brightness + steps 2026-07-10 10:54:22 +08:00
xfy
d5e3008343 test: CPU-mirrored geodesic physics + bcrit/capture unit tests 2026-07-10 10:43:11 +08:00
xfy
265ec9925d feat: BlackHoleUniforms + camera/params mirror system 2026-07-10 10:33:10 +08:00
xfy
1a2b157ef3 feat: BlackHoleParams tunable resource 2026-07-10 10:27:10 +08:00
xfy
d039064efa feat: orbit camera controller (yaw/pitch/zoom) 2026-07-10 10:25:16 +08:00
xfy
a45d42c07a feat: full-screen quad + BlackHoleMaterial pipeline 2026-07-10 10:20:38 +08:00
xfy
9c8f384d0e feat: web build via trunk with WebGPU detection + fallback 2026-07-10 10:12:57 +08:00
xfy
25e2207e0d feat: minimal bevy window opens 2026-07-10 09:48:38 +08:00
xfy
3d34c0920d Initial commit: scaffold Rust project with Cargo 2026-07-09 17:55:14 +08:00