Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .agents/skills/4d-geometry/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
name: 4d-geometry
description: Build or modify Rust4D primitives and tetrahedral meshes. Use when touching rust4d_math::Mesh4D, primitives, Tesseract4D, ShapeTemplate geometry variants, or any code that creates 4D tetrahedra for slicing.
---

# 4D Geometry Workflow

Rust4D renders **3D slices of 4D boundary meshes**. The renderer wants a closed
3-manifold embedded in 4D, decomposed into tetrahedra. If a primitive is not
structurally watertight, the GPU will show cracks, T-junctions, missing faces,
or interior membranes when sliced.

## Mandatory checks for every primitive

1. Construct as `Mesh4D` where possible.
2. Run `mesh.validate()` — catches out-of-bounds and repeated cell indices.
3. Run `mesh.is_watertight()` — every triangular face must be shared by exactly
two tetrahedra.
4. Pin expected cell counts in tests.
5. Pin total boundary 3-volume using `mesh.surface_volume()`:
- exact for regular polytopes,
- convergent-from-below for curved approximations.
6. Render with `cargo run --example shape_showcase .scratchpad/captures-gallery`
and inspect captures.

## Seam rule

For composite curved shapes, shared vertices must share **global indices before
splitting prisms**. Use the `VertexPool` pattern from `primitives/curved.rs`.
Do not rely on post-hoc welding to fix seams: welding after splitting does not
fix mismatched quad diagonals.

Use `primitives::extrude::split_prism`, which applies the Dompierre
lowest-global-index rule so neighboring prisms choose the same diagonals on
shared quad faces.

## Tesseract warning

The tesseract must emit only its 48 boundary tetrahedra. The old 84-tet Kuhn
surface included 36 internal membranes, wasting slice work and rendering
spurious interior walls. Any future tesseract edit must keep:

```rust
assert_eq!(Tesseract4D::new(2.0).tetrahedra().len(), 48);
assert!(Mesh4D::from(&tess as &dyn ConvexShape4D).is_watertight());
```

## Related docs

- `docs/4d-math.md` — slicing and matrix conventions
- `docs/shapes.md` — shape catalog (when updated)
- `.agents/skills/headless-visual-verification` — GPU capture workflow
57 changes: 57 additions & 0 deletions .agents/skills/headless-visual-verification/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
name: headless-visual-verification
description: Run autonomous visual checks for Rust4D rendering changes. Use after touching WGSL shaders, RenderUniforms, camera projection, slice pipeline, primitives, or examples that affect rendering.
---

# Headless Visual Verification

Willow does not do manual rendering checks for this project. If a change affects
visual output, verify it yourself with offscreen GPU captures.

## Slice invariant protocol

Use for camera/movement/physics/slice correctness:

```bash
nix develop --command cargo run --example headless_protocol .scratchpad/captures
```

Read the `[STATE] slice_w(...)` logs. During WASD phases, slice W must remain
constant. Convert frames with ImageMagick if needed.

## Primitive showcase protocol

Use for geometry, shader, material, and lighting work:

```bash
nix develop --command cargo run --example shape_showcase .scratchpad/captures-gallery
```

Expected:
- 81 captures (9 primitives × 3 slice offsets × 3 orientations)
- zero zero-triangle captures
- no cracks/T-junctions/hairline seams
- central slices of every primitive visibly distinct

Make a quick contact sheet:

```bash
mkdir -p .scratchpad/captures-gallery/png
for f in .scratchpad/captures-gallery/*_mid_identity.ppm; do
magick "$f" ".scratchpad/captures-gallery/png/$(basename "${f%.ppm}").png"
done
magick .scratchpad/captures-gallery/png/tesseract_mid_identity.png \
.scratchpad/captures-gallery/png/hypersphere_mid_identity.png \
.scratchpad/captures-gallery/png/pentachoron_mid_identity.png +append row1.png
# Repeat rows, then `magick row1.png row2.png row3.png -append contact-sheet.png`
```

Use `+append`/`-append`; ImageMagick `montage` may need fonts unavailable in
the nix shell.

## GPU warning

The current wgpu/naga stack may emit Vulkan validation warning
`VUID-StandaloneSpirv-MemorySemantics-10871` for `OpAtomicIAdd` relaxed
semantics. This is harmless for now and tracked as a wgpu upgrade backlog item.
Do not confuse it with a rendering failure.
59 changes: 59 additions & 0 deletions .agents/skills/production-readiness/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
name: production-readiness
description: Final quality gate for Rust4D feature branches. Use before PRs, releases, or after broad engine changes. Covers formatting, tests, clippy, rustdoc, visual captures, docs, and scratchpad handoff.
---

# Production Readiness Gate

Run this before opening a PR or calling a feature branch done.

## Code gate

```bash
nix develop --command cargo fmt --all -- --check
nix develop --command cargo clippy --workspace --all-targets -- -D warnings
nix develop --command bash -c 'RUSTDOCFLAGS=-Dwarnings cargo doc --workspace --no-deps'
nix develop --command cargo test --workspace
nix develop --command cargo test --test slice_invariant
```

No warnings. No ignored failures unless already documented.

## Visual gate

For rendering-affecting work:

```bash
nix develop --command cargo run --example shape_showcase .scratchpad/captures-gallery
nix develop --command cargo run --example headless_protocol .scratchpad/captures
```

Inspect at least one contact sheet or representative PNGs. Do not ask Willow to
verify visually.

## Docs gate

- README feature list updated
- `docs/README.md` links any new doc
- `docs/developer-guide.md` updated for workflows/architecture changes
- `docs/4d-math.md` updated for math/convention changes
- `docs/shapes.md` updated for primitive changes
- `.agents/skills/*` updated if a workflow changed
- Rustdoc has no broken intra-doc links (`RUSTDOCFLAGS=-Dwarnings` catches this)

## Scratchpad gate

- Update the active plan with completed waves and next steps
- Update `scratchpad/board.md` with correct `# ` column names
- Write a short report if ending a substantial session
- Commit and push scratchpad separately from repo code

## PR gate

PR body should include:
- Summary
- Why it matters / root cause if a fix
- Verification commands and visual evidence
- Known follow-ups

Mention exact test counts and capture counts where relevant.
54 changes: 54 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: CI

on:
push:
branches: [main, feature/**]
pull_request:
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -Dwarnings
RUSTDOCFLAGS: -Dwarnings

jobs:
test:
name: clippy, docs, tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libasound2-dev \
libudev-dev \
pkg-config \
libwayland-dev \
libxkbcommon-dev

- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt

- name: Cache cargo registry and target
uses: Swatinem/rust-cache@v2

- name: Check formatting
run: cargo fmt --all -- --check

- name: Clippy
run: cargo clippy --workspace --all-targets -- -D warnings

- name: Documentation
run: cargo doc --workspace --no-deps

- name: Tests
run: cargo test --workspace
78 changes: 78 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Changelog

All notable changes to Rust4D are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
Conventional Commits.

## Unreleased — Engine Expansion

### Added

- General `Mesh4D` tetrahedral mesh type with merge, transform, weld,
validation, Gram-determinant cell volumes, and watertightness checks.
- Full primitive catalog:
- tesseract (fixed boundary-only tetrahedralization),
- hypersphere,
- regular 5-cell, 16-cell, 24-cell, 600-cell,
- spherinder,
- cubinder,
- duocylinder.
- RON `ShapeTemplate` variants for all primitives, including defaulted
resolution fields.
- Shape-aware physics collider hints for scene instantiation.
- `scenes/gallery.ron` with all primitive exhibits.
- `examples/shape_showcase.rs`, an offscreen visual verification harness for
the full primitive catalog.
- Two-sided Blinn-Phong lighting with specular highlights, point lights, and
distance fog.
- `rust4d_input::ActionMap` and `CameraAction` for semantic camera bindings.
- Lua ECS entity handle bit round-tripping via `world.entity_from_bits(bits)`
and `entity:equals(other)`.
- GitHub Actions CI: formatting, clippy with `-D warnings`, rustdoc with
`-D warnings`, and workspace tests.
- Project skills for 4D geometry, headless visual verification, and production
readiness.
- Shape catalog documentation (`docs/shapes.md`).

### Changed

- `CameraController` now processes semantic actions from an `ActionMap` while
preserving the legacy keyboard defaults.
- Workspace is now `rustfmt` clean.
- Rendering disables back-face culling because slice-generated triangle winding
is not stable across all marching-tetrahedra cases.

### Fixed

- Tesseract geometry now emits only the 48 boundary tetrahedra. The previous
84-tetrahedron Kuhn-derived surface included 36 internal membranes, wasting
GPU slice work and producing spurious interior walls when viewed from inside.

## PR #15 — 4D Rendering Debug Fix

### Added

- `tests/slice_invariant.rs`, an end-to-end invariant suite for camera,
physics, controller, and simulation movement.
- `examples/headless_protocol.rs`, an offscreen GPU visual verification harness
for slice-plane drift and projection issues.
- `flake.nix` dev shell with Rust, Vulkan wiring, lavapipe, and image tools.
- `docs/4d-math.md`, documenting rotors, SkipY, slicing, projection, movement
invariants, and matrix conventions.
- Minimal `AGENTS.md` plus progressive-disclosure skills.

### Fixed

- Long-standing 4D movement bug: WASD movement after 4D rotation drifted across
the slice plane because world axes were scaled anisotropically. Speeds now
scale semantic movement inputs instead.
- Perspective matrix depth range now matches wgpu `[0, 1]` rather than OpenGL
`[-1, 1]`.
- `rotate_w` and `rotate_xw` now operate in their documented 4D planes after
SkipY remapping.
- Removed dead `camera_eye` from `SliceParams`.

### Quality

- Workspace clippy-clean and rustdoc-clean at merge time.
- Windowed and headless visual verification performed.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ This is not just a visualization trick - the engine actually computes 4D geometr

## Features

- **True 4D Geometry**: All primitives are mathematically defined in 4D space
- **True 4D Geometry**: tesseract, hypersphere, spherinder, cubinder, duocylinder, and the regular 5-cell/16-cell/24-cell/600-cell — all mathematically defined in 4D space
- **GPU-Accelerated Slicing**: Compute shaders slice tetrahedra in parallel
- **4D Physics**: Gravity, collision detection, and rigid body dynamics in 4D
- **FPS-Style Controls**: Navigate 4D space with intuitive WASD + Q/E controls
Expand Down Expand Up @@ -194,6 +194,7 @@ See [examples/README.md](examples/README.md) for the full example index and lear
| [User Guide](docs/user-guide.md) — full feature reference | All users |
| [Developer Guide](docs/developer-guide.md) — architecture, algorithms, testing, contributing | Contributors |
| [The Mathematics of Rust4D](docs/4d-math.md) — rotors, SkipY, slicing, the slice invariant, conventions | Contributors |
| [Shape Catalog](docs/shapes.md) — built-in 4D primitives, construction math, RON snippets, verification | Users & contributors |
| [ARCHITECTURE.md](ARCHITECTURE.md) — crate structure and data flow | Contributors |

## Inspiration
Expand Down
34 changes: 17 additions & 17 deletions crates/rust4d_audio/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ impl AudioEngine4D {
/// Create a new audio engine
pub fn new() -> Result<Self, AudioError> {
let manager = AudioManager::<DefaultBackend>::new(AudioManagerSettings::default())
.map_err(|e: kira::manager::backend::cpal::Error| AudioError::ManagerInit(e.to_string()))?;
.map_err(|e: kira::manager::backend::cpal::Error| {
AudioError::ManagerInit(e.to_string())
})?;

let mut engine = Self {
manager,
Expand Down Expand Up @@ -151,11 +153,10 @@ impl AudioEngine4D {
/// Returns `AudioError::SoundIdOverflow` if the maximum number of sounds
/// (2^64) has been reached. In practice this is unreachable.
pub fn load_sound(&mut self, path: &str) -> Result<SoundHandle, AudioError> {
let sound_data = StaticSoundData::from_file(path)
.map_err(|e| AudioError::LoadSound {
path: path.to_string(),
message: e.to_string(),
})?;
let sound_data = StaticSoundData::from_file(path).map_err(|e| AudioError::LoadSound {
path: path.to_string(),
message: e.to_string(),
})?;

let id = self.next_sound_id;
self.next_sound_id = self
Expand Down Expand Up @@ -183,15 +184,13 @@ impl AudioEngine4D {
let settings = StaticSoundSettings::new().output_destination(track);
let sound_with_settings = sound_data.with_settings(settings);

let handle = self.manager
let handle = self
.manager
.play(sound_with_settings)
.map_err(|e| AudioError::PlaySound(e.to_string()))?;

// Track the sound handle for stop_all/stop_bus support
self.active_sounds
.entry(bus)
.or_default()
.push(handle);
self.active_sounds.entry(bus).or_default().push(handle);

Ok(())
}
Expand Down Expand Up @@ -232,15 +231,13 @@ impl AudioEngine4D {

let sound_with_settings = sound_data.with_settings(settings);

let handle = self.manager
let handle = self
.manager
.play(sound_with_settings)
.map_err(|e| AudioError::PlaySound(e.to_string()))?;

// Track the sound handle for stop_all/stop_bus support
self.active_sounds
.entry(bus)
.or_default()
.push(handle);
self.active_sounds.entry(bus).or_default().push(handle);

log::trace!(
"Playing spatial sound at {:?}, volume: {:.2}, panning: {:.2}",
Expand All @@ -266,7 +263,10 @@ impl AudioEngine4D {
/// Set the volume of a specific bus
pub fn set_bus_volume(&mut self, bus: AudioBus, volume: f32) {
if let Some(track) = self.bus_tracks.get_mut(&bus) {
track.set_volume(Volume::Amplitude(volume.clamp(0.0, 1.0) as f64), Tween::default());
track.set_volume(
Volume::Amplitude(volume.clamp(0.0, 1.0) as f64),
Tween::default(),
);
log::debug!("Set {:?} bus volume to {:.2}", bus, volume);
}
}
Expand Down
Loading
Loading