Compare commits

..

4 Commits

Author SHA1 Message Date
Jonas H
e6c8c259e7 refactor 2026-03-28 11:16:06 +01:00
Jonas H
a79c824540 +1 2026-03-28 11:15:48 +01:00
Jonas H
1ad7b94386 agent update 2026-03-28 11:15:41 +01:00
Jonas H
d21a467878 claude md update 2026-03-28 11:09:02 +01:00
14 changed files with 539 additions and 472 deletions

View File

@@ -1,32 +0,0 @@
---
name: explore
description: Explore the codebase to answer architecture questions, locate files, and understand how systems interact. Use this before reading files in the main context. Returns targeted file paths and concise context. Examples: "where does the rule system read from?", "what storages does drag_system touch?", "how does level loading work?"
model: claude-haiku-4-5-20251001
tools:
- mcp__plugin_qmd_qmd__query
- mcp__plugin_qmd_qmd__get
- mcp__opty__opty_query
- mcp__opty__opty_ast
- Glob
- Grep
- Read
---
You are a codebase exploration agent for the snow trail project. Your job is to answer questions about the codebase as concisely as possible.
## Priority order for information sources
1. **QMD first** — search the `brain-project` collection with `mcp__plugin_qmd_qmd__query` using lex/vec/hyde sub-queries. Best for architecture and design patterns.
2. **Opty** — use `mcp__opty__opty_query` for semantic code search (finding functions, types, system interactions). Use `mcp__opty__opty_ast` for exploring file structure and dependencies.
3. **Glob/Grep** — when you need exact pattern matching or file location by name.
4. **Read** — only read specific files when you need precise detail (e.g. function signatures, exact field names). Prefer small files or targeted line ranges.
## Output format
Return a compact summary with:
- The direct answer to the question
- Relevant `file:line` references for anything the caller will need to edit
- No code blocks unless a snippet is essential to the answer
- No re-stating of what you searched — just the findings
Do not read entire large files. If you need to confirm a type or function signature, use Grep to find the definition line, then Read a narrow range around it.

View File

@@ -1,24 +0,0 @@
# Build Commands
## Desktop
```bash
cargo build # debug
cargo build --release # release
cargo run # game mode
cargo run -- --editor # editor mode
```
## iOS
iOS builds require macOS. The project uses a custom SDL3 + wgpu iOS export pipeline. See `brain-project/ios/readme.md` in QMD for the full export guide.
## Android
```bash
cargo apk build
```
## Checks
```bash
cargo check
cargo fmt
cargo clippy
```

View File

@@ -1,4 +1,8 @@
# WGSL Uniform Buffer Alignment # WGSL Shader Development
Reference for WGSL shader development, buffer alignment, uniform struct layout, and shader asset management. Load this skill when working on shader code, graphics pipelines, or buffer alignment issues.
## WGSL Uniform Buffer Alignment
When creating uniform buffers for WGSL shaders, struct fields must be aligned: When creating uniform buffers for WGSL shaders, struct fields must be aligned:
@@ -12,4 +16,6 @@ When creating uniform buffers for WGSL shaders, struct fields must be aligned:
Use padding fields to match WGSL struct layout exactly. Prefer `vec4` over individual floats to avoid alignment issues. Use padding fields to match WGSL struct layout exactly. Prefer `vec4` over individual floats to avoid alignment issues.
## Shader Organization
Shaders live in `src/shaders/` and are embedded via `include_str!()`. Shaders live in `src/shaders/` and are embedded via `include_str!()`.

30
.pi/settings.local.json Normal file
View File

@@ -0,0 +1,30 @@
{
"permissions": {
"allow": [
"Bash(cargo check:*)",
"Bash(cargo build:*)",
"Bash(cargo fmt:*)",
"Bash(head:*)",
"mcp__plugin_qmd_qmd__deep_search",
"mcp__plugin_qmd_qmd__query",
"mcp__opty__opty_status",
"mcp__opty__opty_query",
"mcp__opty__opty_ast",
"Bash(cargo search:*)",
"Bash(cargo info:*)"
]
},
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/filter-cargo-warnings.py\""
}
]
}
]
}
}

View File

@@ -10,6 +10,19 @@ Pure Rust game: SDL3 windowing, wgpu rendering, rapier3d physics, low-res retro
- **NO inline paths** — always add `use` statements at the top of files, never inline - **NO inline paths** — always add `use` statements at the top of files, never inline
- **NO `use` statements inside functions or impl blocks** — all `use` must be at the file (module) level - **NO `use` statements inside functions or impl blocks** — all `use` must be at the file (module) level
**Storage Parameters:**
- Functions should take specific storages they need rather than `&World` or `&mut World`
- Pass individual fields (`&world.transforms`, `&mut world.state_machines`) at the call site
- This makes data dependencies explicit for both the borrow checker and the reader
## Architecture ## Architecture
Pure ECS: entities are IDs, components are plain data in `HashMap<EntityHandle, T>` storages, systems are functions receiving `&mut World`. No `Rc<RefCell<>>`. Pure ECS: entities are IDs, components are plain data in `HashMap<EntityHandle, T>` storages, systems are functions receiving `&mut World`. No `Rc<RefCell<>>`.
## Sub-Agents & Codebase Exploration
**Use the `explorer` sub-agent for all codebase work.** Unless the target is trivially obvious (e.g., you already know the exact file path and line number) and unless you are the explorer agent. This includes:
- Understanding existing code before making changes
- Searching for related functions/types
- Investigating bugs or architectural patterns
- Finding usages of a function across the codebase

View File

@@ -9,37 +9,24 @@ mod physics;
mod picking; mod picking;
mod postprocess; mod postprocess;
mod render; mod render;
mod snow;
mod snow_light;
mod state; mod state;
mod systems; mod systems;
mod texture; mod texture;
mod utility; mod utility;
mod world; mod world;
use crate::debug::{collider_debug, DebugMode};
use crate::editor::{editor_loop, EditorState, FrameStats};
use std::time::{Duration, Instant};
use glam::Vec3;
use render::Renderer;
use sdl3::event::Event;
use sdl3::keyboard::Keycode;
use sdl3::mouse::MouseButton;
use utility::input::InputState;
use world::World;
use crate::bundles::camera::CameraBundle; use crate::bundles::camera::CameraBundle;
use crate::bundles::player::PlayerBundle; use crate::bundles::player::PlayerBundle;
use crate::bundles::spotlight::spawn_spotlights; use crate::bundles::spotlight::spawn_spotlights;
use crate::bundles::terrain::{TerrainBundle, TerrainConfig}; use crate::bundles::terrain::{TerrainBundle, TerrainConfig};
use crate::bundles::test_char::TestCharBundle; use crate::bundles::test_char::TestCharBundle;
use crate::bundles::Bundle; use crate::bundles::Bundle;
use crate::debug::{collider_debug, DebugMode};
use crate::editor::{editor_loop, EditorState, FrameStats};
use crate::entity::EntityHandle;
use crate::loaders::scene::Space; use crate::loaders::scene::Space;
use crate::physics::PhysicsManager; use crate::physics::PhysicsManager;
use crate::snow::{SnowConfig, SnowLayer}; use crate::render::snow::{SnowConfig, SnowLayer};
use crate::systems::camera::stop_camera_following; use crate::systems::camera::stop_camera_following;
use crate::systems::{ use crate::systems::{
camera_follow_system, camera_input_system, camera_view_matrix, dialog_bubble_render_system, camera_follow_system, camera_input_system, camera_view_matrix, dialog_bubble_render_system,
@@ -49,9 +36,34 @@ use crate::systems::{
tree_dissolve_update_system, tree_instance_buffer_update_system, tree_occlusion_system, tree_dissolve_update_system, tree_instance_buffer_update_system, tree_occlusion_system,
trigger_system, trigger_system,
}; };
use crate::utility::input::InputState;
use crate::utility::time::Time; use crate::utility::time::Time;
fn main() -> Result<(), Box<dyn std::error::Error>> use std::time::{Duration, Instant};
use glam::Vec3;
use render::Renderer;
use sdl3::event::Event;
use sdl3::keyboard::Keycode;
use sdl3::mouse::MouseButton;
use world::World;
struct Game
{
sdl_context: sdl3::Sdl,
window: sdl3::video::Window,
_event_pump: sdl3::EventPump,
world: World,
editor: EditorState,
input_state: InputState,
camera_entity: EntityHandle,
last_frame: Instant,
frame_duration: Duration,
physics_accumulator: f32,
stats: FrameStats,
}
fn init() -> Result<Game, Box<dyn std::error::Error>>
{ {
let sdl_context = sdl3::init()?; let sdl_context = sdl3::init()?;
let video_subsystem = sdl_context.video()?; let video_subsystem = sdl_context.video()?;
@@ -72,34 +84,69 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
}); });
editor.init_platform(&window); editor.init_platform(&window);
let (mut world, camera_entity) = init_world()?;
start_camera_following(&mut world, camera_entity);
let _event_pump = sdl_context.event_pump()?;
let input_state = InputState::new();
sdl_context.mouse().set_relative_mouse_mode(&window, true);
Time::init();
Ok(Game {
sdl_context,
window,
_event_pump,
world,
editor,
input_state,
camera_entity,
last_frame: Instant::now(),
frame_duration: Duration::from_millis(1000 / 60),
physics_accumulator: 0.0,
stats: FrameStats {
fps: 0.0,
frame_ms: 0.0,
physics_budget_ms: 0.0,
draw_call_count: 0,
},
})
}
fn init_world() -> Result<(World, EntityHandle), Box<dyn std::error::Error>>
{
let space = Space::load_space(&crate::paths::meshes::terrain())?; let space = Space::load_space(&crate::paths::meshes::terrain())?;
let terrain_config = TerrainConfig::default(); let terrain_config = TerrainConfig::default();
let player_spawn = space.player_spawn;
let camera_spawn = space.camera_spawn_position();
let tree_positions: Vec<Vec3> = space let tree_positions: Vec<Vec3> = space
.mesh_data .mesh_data
.iter() .iter()
.flat_map(|(_, instances)| instances.iter().map(|inst| inst.position)) .flat_map(|(_, instances)| instances.iter().map(|inst| inst.position))
.collect(); .collect();
let player_spawn = space.player_spawn;
let test_char_spawn = space.test_char_spawn;
let camera_spawn = space.camera_spawn_position();
let spotlights = space.spotlights;
let mesh_data = space.mesh_data;
let mut world = World::new(); let mut world = World::new();
let _player_entity = PlayerBundle { PlayerBundle {
position: player_spawn, position: player_spawn,
} }
.spawn(&mut world) .spawn(&mut world)
.unwrap(); .unwrap();
let _test_char_entity = TestCharBundle { TestCharBundle {
position: space.test_char_spawn, position: test_char_spawn,
} }
.spawn(&mut world) .spawn(&mut world)
.unwrap(); .unwrap();
let _terrain_entity = TerrainBundle::spawn(&mut world, space.mesh_data, &terrain_config)?; TerrainBundle::spawn(&mut world, mesh_data, &terrain_config)?;
spawn_spotlights(&mut world, space.spotlights); spawn_spotlights(&mut world, spotlights);
render::set_terrain_data(); render::set_terrain_data();
@@ -110,64 +157,38 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
); );
let snow_config = SnowConfig::default(); let snow_config = SnowConfig::default();
let mut snow_layer = SnowLayer::load(&snow_config)?; let snow_layer = SnowLayer::load(&snow_config)?;
for pos in &tree_positions for pos in &tree_positions
{ {
snow_layer.deform_at_position(*pos, 5.0, 50.0); snow_layer.deform_at_position(*pos, 5.0, 50.0);
} }
println!("Snow layer loaded successfully"); println!("Snow layer loaded successfully");
render::set_snow_depth(&snow_layer.depth_texture_view); render::set_snow_depth(&snow_layer.depth_texture_view);
world.snow_layer = Some(snow_layer);
let mut debug_mode = DebugMode::default();
let camera_entity = CameraBundle { let camera_entity = CameraBundle {
position: camera_spawn, position: camera_spawn,
} }
.spawn(&mut world) .spawn(&mut world)
.unwrap(); .unwrap();
start_camera_following(&mut world, camera_entity);
let _event_pump = sdl_context.event_pump()?; Ok((world, camera_entity))
let mut input_state = InputState::new(); }
sdl_context.mouse().set_relative_mouse_mode(&window, true); fn process_events(game: &mut Game) -> bool
{
Time::init(); game.editor.begin_frame();
let mut last_frame = Instant::now();
let target_fps = 60;
let frame_duration = Duration::from_millis(1000 / target_fps);
const FIXED_TIMESTEP: f32 = 1.0 / 60.0;
let mut physics_accumulator = 0.0;
let mut stats = FrameStats {
fps: 0.0,
frame_ms: 0.0,
physics_budget_ms: 0.0,
draw_call_count: 0,
};
'running: loop
{
let frame_start = Instant::now();
let time = Time::get_time_elapsed();
let delta = (frame_start - last_frame).as_secs_f32();
last_frame = frame_start;
editor.begin_frame();
while let Some(raw_event) = dear_imgui_sdl3::sdl3_poll_event_ll() while let Some(raw_event) = dear_imgui_sdl3::sdl3_poll_event_ll()
{ {
editor.process_event(&raw_event); game.editor.process_event(&raw_event);
let event = Event::from_ll(raw_event); let event = Event::from_ll(raw_event);
match &event match &event
{ {
Event::Quit { .. } => Event::Quit { .. } =>
{ {
input_state.quit_requested = true; return true;
continue;
} }
Event::KeyDown { Event::KeyDown {
@@ -176,43 +197,34 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
.. ..
} => } =>
{ {
editor.active = !editor.active; toggle_editor(game);
if editor.active
{
stop_camera_following(&mut world, camera_entity);
sdl_context.mouse().set_relative_mouse_mode(&window, false);
editor.right_mouse_held = false;
input_state.mouse_captured = false;
}
else
{
start_camera_following(&mut world, camera_entity);
input_state.mouse_captured = true;
sdl_context.mouse().set_relative_mouse_mode(&window, true);
}
continue; continue;
} }
Event::MouseButtonDown { Event::MouseButtonDown {
mouse_btn: MouseButton::Right, mouse_btn: MouseButton::Right,
.. ..
} if editor.active => } if game.editor.active =>
{ {
editor.right_mouse_held = true; game.editor.right_mouse_held = true;
input_state.mouse_captured = true; game.input_state.mouse_captured = true;
stop_camera_following(&mut world, camera_entity); stop_camera_following(&mut game.world, game.camera_entity);
sdl_context.mouse().set_relative_mouse_mode(&window, true); game.sdl_context
.mouse()
.set_relative_mouse_mode(&game.window, true);
continue; continue;
} }
Event::MouseButtonUp { Event::MouseButtonUp {
mouse_btn: MouseButton::Right, mouse_btn: MouseButton::Right,
.. ..
} if editor.active => } if game.editor.active =>
{ {
editor.right_mouse_held = false; game.editor.right_mouse_held = false;
input_state.mouse_captured = false; game.input_state.mouse_captured = false;
sdl_context.mouse().set_relative_mouse_mode(&window, false); game.sdl_context
.mouse()
.set_relative_mouse_mode(&game.window, false);
continue; continue;
} }
@@ -221,26 +233,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
x, x,
y, y,
.. ..
} if editor.active && !editor.wants_mouse() => } if game.editor.active && !game.editor.wants_mouse() =>
{ {
if let Some(view) = crate::systems::camera_view_matrix(&world) handle_editor_pick(game, *x, *y);
{
if let Some((_, cam)) = world.active_camera()
{
let projection = cam.projection_matrix();
let (win_w, win_h) = window.size();
let ray = crate::picking::Ray::from_screen_position(
*x,
*y,
win_w,
win_h,
&view,
&projection,
);
editor.selected_entity = crate::picking::pick_entity(&ray, &world);
render::set_selected_entity(editor.selected_entity);
}
}
continue; continue;
} }
@@ -248,132 +243,102 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
{} {}
} }
if editor.active && (editor.wants_keyboard() || editor.wants_mouse()) if game.editor.active && (game.editor.wants_keyboard() || game.editor.wants_mouse())
{ {
continue; continue;
} }
let capture_changed = input_state.handle_event(&event); let capture_changed = game.input_state.handle_event(&event);
if capture_changed && !editor.active if capture_changed && !game.editor.active
{ {
sdl_context game.sdl_context
.mouse() .mouse()
.set_relative_mouse_mode(&window, input_state.mouse_captured); .set_relative_mouse_mode(&game.window, game.input_state.mouse_captured);
} }
} }
if input_state.quit_requested false
}
fn toggle_editor(game: &mut Game)
{
game.editor.active = !game.editor.active;
if game.editor.active
{ {
break 'running; stop_camera_following(&mut game.world, game.camera_entity);
} game.sdl_context
.mouse()
if input_state.debug_cycle_just_pressed .set_relative_mouse_mode(&game.window, false);
{ game.editor.right_mouse_held = false;
debug_mode = debug_mode.cycle(); game.input_state.mouse_captured = false;
println!("Debug mode: {:?}", debug_mode);
}
if input_state.f2_just_pressed
{
editor.show_player_state = !editor.show_player_state;
}
camera_input_system(&mut world, &input_state);
if editor.active
{
editor_loop(&mut editor, &mut world, &input_state, &stats, delta);
} }
else else
{ {
let dialog_active = !world.bubble_tags.all().is_empty(); start_camera_following(&mut game.world, game.camera_entity);
if dialog_active game.input_state.mouse_captured = true;
{ game.sdl_context
dialog_camera_system(&mut world, delta); .mouse()
.set_relative_mouse_mode(&game.window, true);
} }
else }
fn handle_editor_pick(game: &mut Game, x: f32, y: f32)
{
let view = match camera_view_matrix(&game.world)
{ {
camera_follow_system(&mut world); Some(v) => v,
} None => return,
player_input_system(&mut world, &input_state); };
if editor.show_player_state let (_, cam) = match game.world.active_camera()
{ {
editor.build_hud(&world); Some(c) => c,
} None => return,
} };
let projection = cam.projection_matrix();
let (win_w, win_h) = game.window.size();
let ray = picking::Ray::from_screen_position(x, y, win_w, win_h, &view, &projection);
game.editor.selected_entity = picking::pick_entity(&ray, &game.world);
render::set_selected_entity(game.editor.selected_entity);
}
let physics_start = Instant::now(); fn submit_frame(game: &mut Game, draw_calls: &[render::DrawCall], time: f32, delta: f32)
{
physics_accumulator += delta; let (camera_entity, camera_component) = match game.world.active_camera()
while physics_accumulator >= FIXED_TIMESTEP
{ {
state_machine_physics_system(&mut world, FIXED_TIMESTEP); Some(c) => c,
None => return,
PhysicsManager::physics_step(); };
let camera_transform = match game.world.transforms.get(camera_entity)
physics_sync_system(&mut world);
trigger_system(&mut world);
dialog_system(&mut world, FIXED_TIMESTEP);
dialog_projectile_system(&mut world, &input_state);
physics_accumulator -= FIXED_TIMESTEP;
}
stats.physics_budget_ms = physics_start.elapsed().as_secs_f32() * 1000.0;
state_machine_system(&mut world, delta);
rotate_system(&mut world, delta);
tree_occlusion_system(&mut world);
tree_dissolve_update_system(&mut world, delta);
tree_instance_buffer_update_system(&mut world);
let spotlights = spotlight_sync_system(&world);
render::update_spotlights(spotlights);
snow_system(&world, &mut snow_layer, editor.active);
let mut draw_calls = render_system(&world);
draw_calls.extend(snow_layer.get_draw_calls());
if debug_mode == DebugMode::Colliders
{ {
draw_calls.extend(collider_debug::render_collider_debug()); Some(t) => t,
} None => return,
};
let view = match camera_view_matrix(&game.world)
{
Some(v) => v,
None => return,
};
if let Some((camera_entity, camera_component)) = world.active_camera()
{
if let Some(camera_transform) = world.transforms.get(camera_entity)
{
let player_pos = world.player_position();
if let Some(view) = camera_view_matrix(&world)
{
let projection = camera_component.projection_matrix(); let projection = camera_component.projection_matrix();
let view_proj = projection * view; let view_proj = projection * view;
let player_pos = game.world.player_position();
let billboard_calls = let billboard_calls =
dialog_bubble_render_system(&world, camera_transform.position, view_proj); dialog_bubble_render_system(&game.world, camera_transform.position, view_proj);
stats.draw_call_count = draw_calls.len();
stats.fps = 1.0 / delta;
stats.frame_ms = delta * 1000.0;
let frame = render::render( let frame = render::render(
&view, &view,
&projection, &projection,
camera_transform.position, camera_transform.position,
player_pos, player_pos,
&draw_calls, draw_calls,
&billboard_calls, &billboard_calls,
time, time,
delta, delta,
debug_mode, game.world.debug_mode,
); );
if editor.active || editor.show_player_state if game.editor.active || game.editor.show_player_state
{ {
let screen_view = frame let screen_view = frame
.texture .texture
@@ -383,21 +348,129 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
label: Some("ImGui Encoder"), label: Some("ImGui Encoder"),
}) })
}); });
editor.render(&mut encoder, &screen_view); game.editor.render(&mut encoder, &screen_view);
render::with_queue(|q| q.submit(std::iter::once(encoder.finish()))); render::with_queue(|q| q.submit(std::iter::once(encoder.finish())));
} }
frame.present(); frame.present();
}
const FIXED_TIMESTEP: f32 = 1.0 / 60.0;
fn main() -> Result<(), Box<dyn std::error::Error>>
{
let mut game = init()?;
loop
{
let frame_start = Instant::now();
let time = Time::get_time_elapsed();
let delta = (frame_start - game.last_frame).as_secs_f32();
game.last_frame = frame_start;
// --- events ---
if process_events(&mut game)
{
break;
} }
if game.input_state.debug_cycle_just_pressed
{
game.world.debug_mode = game.world.debug_mode.cycle();
println!("Debug mode: {:?}", game.world.debug_mode);
}
if game.input_state.f2_just_pressed
{
game.editor.show_player_state = !game.editor.show_player_state;
}
// --- camera + input ---
camera_input_system(&mut game.world, &game.input_state);
if game.editor.active
{
editor_loop(
&mut game.editor,
&mut game.world,
&game.input_state,
&game.stats,
delta,
);
}
else
{
let dialog_active = !game.world.bubble_tags.all().is_empty();
if dialog_active
{
dialog_camera_system(&mut game.world, delta);
}
else
{
camera_follow_system(&mut game.world);
}
player_input_system(&mut game.world, &game.input_state);
if game.editor.show_player_state
{
game.editor.build_hud(&game.world);
} }
} }
input_state.clear_just_pressed(); // --- fixed-step physics ---
let physics_start = Instant::now();
game.physics_accumulator += delta;
while game.physics_accumulator >= FIXED_TIMESTEP
{
state_machine_physics_system(&mut game.world, FIXED_TIMESTEP);
PhysicsManager::physics_step();
physics_sync_system(&mut game.world);
trigger_system(&mut game.world);
dialog_system(&mut game.world, FIXED_TIMESTEP);
dialog_projectile_system(&mut game.world, &game.input_state);
game.physics_accumulator -= FIXED_TIMESTEP;
}
game.stats.physics_budget_ms = physics_start.elapsed().as_secs_f32() * 1000.0;
// --- per-frame systems ---
state_machine_system(&mut game.world, delta);
rotate_system(&mut game.world, delta);
tree_occlusion_system(&mut game.world);
tree_dissolve_update_system(&mut game.world, delta);
tree_instance_buffer_update_system(&mut game.world);
let spotlights = spotlight_sync_system(&game.world);
render::update_spotlights(spotlights);
snow_system(&mut game.world, game.editor.active);
// --- draw call collection ---
let mut draw_calls = render_system(&game.world);
if let Some(ref snow_layer) = game.world.snow_layer
{
draw_calls.extend(snow_layer.get_draw_calls());
}
if game.world.debug_mode == DebugMode::Colliders
{
draw_calls.extend(collider_debug::render_collider_debug());
}
game.stats.draw_call_count = draw_calls.len();
game.stats.fps = 1.0 / delta;
game.stats.frame_ms = delta * 1000.0;
// --- render ---
submit_frame(&mut game, &draw_calls, time, delta);
// --- end frame ---
game.input_state.clear_just_pressed();
let frame_time = frame_start.elapsed(); let frame_time = frame_start.elapsed();
if frame_time < frame_duration if frame_time < game.frame_duration
{ {
std::thread::sleep(frame_duration - frame_time); std::thread::sleep(game.frame_duration - frame_time);
} }
} }

127
src/render/global.rs Normal file
View File

@@ -0,0 +1,127 @@
/// Global renderer access via thread-local storage.
///
/// This module isolates the global singleton pattern used to give systems
/// and loaders access to the wgpu `Device` and `Queue` without threading
/// them through every call site. The long-term goal is to replace these
/// with explicit `RenderContext` parameters on systems that need GPU access.
use std::cell::RefCell;
use crate::debug::DebugMode;
use crate::entity::EntityHandle;
use super::{BillboardDrawCall, DrawCall, Renderer, Spotlight};
thread_local! {
static GLOBAL_RENDERER: RefCell<Option<Renderer>> = RefCell::new(None);
}
fn with_ref<F, R>(f: F) -> R
where
F: FnOnce(&Renderer) -> R,
{
GLOBAL_RENDERER.with(|r| {
let renderer = r.borrow();
let renderer = renderer.as_ref().expect("Renderer not initialized");
f(renderer)
})
}
fn with_mut<F, R>(f: F) -> R
where
F: FnOnce(&mut Renderer) -> R,
{
GLOBAL_RENDERER.with(|r| {
let mut renderer = r.borrow_mut();
let renderer = renderer.as_mut().expect("Renderer not initialized");
f(renderer)
})
}
pub fn init(renderer: Renderer)
{
GLOBAL_RENDERER.with(|r| *r.borrow_mut() = Some(renderer));
}
pub fn with_device<F, R>(f: F) -> R
where
F: FnOnce(&wgpu::Device) -> R,
{
with_ref(|r| f(&r.device))
}
pub fn with_queue<F, R>(f: F) -> R
where
F: FnOnce(&wgpu::Queue) -> R,
{
with_ref(|r| f(&r.queue))
}
pub fn with_surface_format<F, R>(f: F) -> R
where
F: FnOnce(wgpu::TextureFormat) -> R,
{
with_ref(|r| f(r.config.format))
}
pub fn aspect_ratio() -> f32
{
with_ref(|r| r.aspect_ratio())
}
pub fn set_terrain_data()
{
with_mut(|r| r.set_terrain_data());
}
pub fn init_snow_light_accumulation(terrain_min: glam::Vec2, terrain_max: glam::Vec2)
{
with_mut(|r| r.init_snow_light_accumulation(terrain_min, terrain_max));
}
pub fn set_snow_depth(snow_depth_view: &wgpu::TextureView)
{
with_mut(|r| r.set_snow_depth(snow_depth_view));
}
#[allow(dead_code)]
pub fn set_shadow_bias(bias: f32)
{
with_mut(|r| r.shadow_bias = bias);
}
pub fn update_spotlights(spotlights: Vec<Spotlight>)
{
with_mut(|r| r.spotlights = spotlights);
}
pub fn set_selected_entity(entity: Option<EntityHandle>)
{
with_mut(|r| r.selected_entity = entity);
}
pub fn render(
view: &glam::Mat4,
projection: &glam::Mat4,
camera_position: glam::Vec3,
player_position: glam::Vec3,
draw_calls: &[DrawCall],
billboard_calls: &[BillboardDrawCall],
time: f32,
delta_time: f32,
debug_mode: DebugMode,
) -> wgpu::SurfaceTexture
{
with_mut(|r| {
r.render(
view,
projection,
camera_position,
player_position,
draw_calls,
billboard_calls,
time,
delta_time,
debug_mode,
)
})
}

View File

@@ -1,11 +1,19 @@
pub mod billboard;
mod bind_group; mod bind_group;
mod debug_overlay; mod debug_overlay;
mod global;
mod pipeline; mod pipeline;
mod shadow; mod shadow;
mod types; mod types;
pub mod billboard;
pub mod snow;
pub mod snow_light;
pub use billboard::{BillboardDrawCall, BillboardPipeline}; pub use billboard::{BillboardDrawCall, BillboardPipeline};
pub use global::{
aspect_ratio, init, init_snow_light_accumulation, render, set_selected_entity, set_snow_depth,
set_terrain_data, update_spotlights, with_device, with_queue, with_surface_format,
};
pub use types::{DrawCall, Pipeline, Spotlight, SpotlightRaw, Uniforms, MAX_SPOTLIGHTS}; pub use types::{DrawCall, Pipeline, Spotlight, SpotlightRaw, Uniforms, MAX_SPOTLIGHTS};
use crate::entity::EntityHandle; use crate::entity::EntityHandle;
@@ -18,7 +26,6 @@ use pipeline::{
create_debug_lines_pipeline, create_main_pipeline, create_snow_clipmap_pipeline, create_debug_lines_pipeline, create_main_pipeline, create_snow_clipmap_pipeline,
create_wireframe_pipeline, create_wireframe_pipeline,
}; };
use std::cell::RefCell;
use std::num::NonZeroU64; use std::num::NonZeroU64;
const MAX_DRAW_CALLS: usize = 64; const MAX_DRAW_CALLS: usize = 64;
@@ -71,7 +78,7 @@ pub struct Renderer
dummy_snow_light_view: wgpu::TextureView, dummy_snow_light_view: wgpu::TextureView,
dummy_snow_light_sampler: wgpu::Sampler, dummy_snow_light_sampler: wgpu::Sampler,
snow_light_accumulation: Option<crate::snow_light::SnowLightAccumulation>, snow_light_accumulation: Option<snow_light::SnowLightAccumulation>,
snow_light_bound: bool, snow_light_bound: bool,
pub selected_entity: Option<EntityHandle>, pub selected_entity: Option<EntityHandle>,
@@ -1037,12 +1044,8 @@ impl Renderer
pub fn init_snow_light_accumulation(&mut self, terrain_min: glam::Vec2, terrain_max: glam::Vec2) pub fn init_snow_light_accumulation(&mut self, terrain_min: glam::Vec2, terrain_max: glam::Vec2)
{ {
let snow_light_accumulation = crate::snow_light::SnowLightAccumulation::new( let snow_light_accumulation =
&self.device, snow_light::SnowLightAccumulation::new(&self.device, terrain_min, terrain_max, 512);
terrain_min,
terrain_max,
512,
);
self.snow_light_accumulation = Some(snow_light_accumulation); self.snow_light_accumulation = Some(snow_light_accumulation);
} }
@@ -1093,137 +1096,3 @@ impl Renderer
self.config.width as f32 / self.config.height as f32 self.config.width as f32 / self.config.height as f32
} }
} }
thread_local! {
static GLOBAL_RENDERER: RefCell<Option<Renderer>> = RefCell::new(None);
}
pub fn init(renderer: Renderer)
{
GLOBAL_RENDERER.with(|r| *r.borrow_mut() = Some(renderer));
}
pub fn with_device<F, R>(f: F) -> R
where
F: FnOnce(&wgpu::Device) -> R,
{
GLOBAL_RENDERER.with(|r| {
let renderer = r.borrow();
let renderer = renderer.as_ref().expect("Renderer not set");
f(&renderer.device)
})
}
pub fn with_queue<F, R>(f: F) -> R
where
F: FnOnce(&wgpu::Queue) -> R,
{
GLOBAL_RENDERER.with(|r| {
let renderer = r.borrow();
let renderer = renderer.as_ref().expect("Renderer not set");
f(&renderer.queue)
})
}
pub fn set_terrain_data()
{
GLOBAL_RENDERER.with(|r| {
let mut renderer = r.borrow_mut();
let renderer = renderer.as_mut().expect("Renderer not set");
renderer.set_terrain_data();
});
}
pub fn init_snow_light_accumulation(terrain_min: glam::Vec2, terrain_max: glam::Vec2)
{
GLOBAL_RENDERER.with(|r| {
let mut renderer = r.borrow_mut();
let renderer = renderer.as_mut().expect("Renderer not set");
renderer.init_snow_light_accumulation(terrain_min, terrain_max);
});
}
pub fn set_snow_depth(snow_depth_view: &wgpu::TextureView)
{
GLOBAL_RENDERER.with(|r| {
let mut renderer = r.borrow_mut();
let renderer = renderer.as_mut().expect("Renderer not set");
renderer.set_snow_depth(snow_depth_view);
});
}
pub fn aspect_ratio() -> f32
{
GLOBAL_RENDERER.with(|r| {
let renderer = r.borrow();
let renderer = renderer.as_ref().expect("Renderer not set");
renderer.aspect_ratio()
})
}
pub fn render(
view: &glam::Mat4,
projection: &glam::Mat4,
camera_position: glam::Vec3,
player_position: glam::Vec3,
draw_calls: &[DrawCall],
billboard_calls: &[BillboardDrawCall],
time: f32,
delta_time: f32,
debug_mode: DebugMode,
) -> wgpu::SurfaceTexture
{
GLOBAL_RENDERER.with(|r| {
let mut renderer = r.borrow_mut();
let renderer = renderer.as_mut().expect("Renderer not set");
renderer.render(
view,
projection,
camera_position,
player_position,
draw_calls,
billboard_calls,
time,
delta_time,
debug_mode,
)
})
}
pub fn with_surface_format<F, R>(f: F) -> R
where
F: FnOnce(wgpu::TextureFormat) -> R,
{
GLOBAL_RENDERER.with(|r| {
let renderer = r.borrow();
let renderer = renderer.as_ref().expect("Renderer not set");
f(renderer.config.format)
})
}
pub fn set_shadow_bias(bias: f32)
{
GLOBAL_RENDERER.with(|r| {
let mut renderer = r.borrow_mut();
let renderer = renderer.as_mut().expect("Renderer not set");
renderer.shadow_bias = bias;
});
}
pub fn update_spotlights(spotlights: Vec<Spotlight>)
{
GLOBAL_RENDERER.with(|r| {
let mut renderer = r.borrow_mut();
let renderer = renderer.as_mut().expect("Renderer not set");
renderer.spotlights = spotlights;
});
}
pub fn set_selected_entity(entity: Option<EntityHandle>)
{
GLOBAL_RENDERER.with(|r| {
let mut renderer = r.borrow_mut();
let renderer = renderer.as_mut().expect("Renderer not set");
renderer.selected_entity = entity;
});
}

View File

@@ -4,10 +4,10 @@ use exr::prelude::{ReadChannels, ReadLayers};
use glam::{Vec2, Vec3}; use glam::{Vec2, Vec3};
use wgpu::util::DeviceExt; use wgpu::util::DeviceExt;
use super::{with_device, with_queue, DrawCall, Pipeline};
use crate::{ use crate::{
loaders::mesh::{InstanceRaw, Mesh, Vertex}, loaders::mesh::{InstanceRaw, Mesh, Vertex},
paths, paths,
render::{self, DrawCall, Pipeline},
texture::HeightmapTexture, texture::HeightmapTexture,
}; };
@@ -78,7 +78,7 @@ pub struct SnowLayer
fn create_instance_buffer() -> wgpu::Buffer fn create_instance_buffer() -> wgpu::Buffer
{ {
render::with_device(|device| { with_device(|device| {
device.create_buffer(&wgpu::BufferDescriptor { device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Snow Clipmap Instance Buffer"), label: Some("Snow Clipmap Instance Buffer"),
size: std::mem::size_of::<InstanceRaw>() as u64, size: std::mem::size_of::<InstanceRaw>() as u64,
@@ -107,13 +107,11 @@ impl SnowLayer
let (deform_pipeline, deform_bind_group, deform_params_buffer) = let (deform_pipeline, deform_bind_group, deform_params_buffer) =
Self::create_deform_pipeline(&depth_texture_view); Self::create_deform_pipeline(&depth_texture_view);
let heightmap_texture = render::with_device(|device| { let heightmap_texture = with_device(|device| {
render::with_queue(|queue| { with_queue(|queue| HeightmapTexture::load(device, queue, &config.heightmap_path))
HeightmapTexture::load(device, queue, &config.heightmap_path)
})
})?; })?;
let depth_sampler = render::with_device(|device| { let depth_sampler = with_device(|device| {
device.create_sampler(&wgpu::SamplerDescriptor { device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("Snow Depth Sampler"), label: Some("Snow Depth Sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_u: wgpu::AddressMode::ClampToEdge,
@@ -226,7 +224,7 @@ impl SnowLayer
height: u32, height: u32,
) -> (wgpu::Texture, wgpu::TextureView, wgpu::BindGroup) ) -> (wgpu::Texture, wgpu::TextureView, wgpu::BindGroup)
{ {
render::with_device(|device| { with_device(|device| {
let size = wgpu::Extent3d { let size = wgpu::Extent3d {
width, width,
height, height,
@@ -248,7 +246,7 @@ impl SnowLayer
let data_bytes: &[u8] = bytemuck::cast_slice(depth_data); let data_bytes: &[u8] = bytemuck::cast_slice(depth_data);
render::with_queue(|queue| { with_queue(|queue| {
queue.write_texture( queue.write_texture(
wgpu::TexelCopyTextureInfo { wgpu::TexelCopyTextureInfo {
texture: &texture, texture: &texture,
@@ -300,7 +298,7 @@ impl SnowLayer
depth_texture_view: &wgpu::TextureView, depth_texture_view: &wgpu::TextureView,
) -> (wgpu::ComputePipeline, wgpu::BindGroup, wgpu::Buffer) ) -> (wgpu::ComputePipeline, wgpu::BindGroup, wgpu::Buffer)
{ {
render::with_device(|device| { with_device(|device| {
let shader_source = std::fs::read_to_string(&paths::shaders::snow_deform()) let shader_source = std::fs::read_to_string(&paths::shaders::snow_deform())
.expect("Failed to load snow deform shader"); .expect("Failed to load snow deform shader");
@@ -383,7 +381,7 @@ impl SnowLayer
depth_sampler: &wgpu::Sampler, depth_sampler: &wgpu::Sampler,
) -> wgpu::BindGroup ) -> wgpu::BindGroup
{ {
render::with_device(|device| { with_device(|device| {
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Snow Displacement Bind Group Layout"), label: Some("Snow Displacement Bind Group Layout"),
entries: &[ entries: &[
@@ -490,7 +488,7 @@ impl SnowLayer
} }
} }
let vertex_buffer = render::with_device(|device| { let vertex_buffer = with_device(|device| {
device.create_buffer_init(&wgpu::util::BufferInitDescriptor { device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(&format!("Snow Clipmap Level {} Vertex Buffer", level)), label: Some(&format!("Snow Clipmap Level {} Vertex Buffer", level)),
contents: bytemuck::cast_slice(&vertices), contents: bytemuck::cast_slice(&vertices),
@@ -498,7 +496,7 @@ impl SnowLayer
}) })
}); });
let index_buffer = render::with_device(|device| { let index_buffer = with_device(|device| {
device.create_buffer_init(&wgpu::util::BufferInitDescriptor { device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(&format!("Snow Clipmap Level {} Index Buffer", level)), label: Some(&format!("Snow Clipmap Level {} Index Buffer", level)),
contents: bytemuck::cast_slice(&indices), contents: bytemuck::cast_slice(&indices),
@@ -542,7 +540,7 @@ impl SnowLayer
} }
} }
render::with_queue(|queue| { with_queue(|queue| {
for (level, clipmap_level) in self.levels.iter().enumerate() for (level, clipmap_level) in self.levels.iter().enumerate()
{ {
let cell_size = self.clipmap_config.base_cell_size * (1u32 << level) as f32; let cell_size = self.clipmap_config.base_cell_size * (1u32 << level) as f32;
@@ -589,7 +587,7 @@ impl SnowLayer
pub fn deform_at_position(&self, position: Vec3, radius: f32, depth: f32) pub fn deform_at_position(&self, position: Vec3, radius: f32, depth: f32)
{ {
render::with_queue(|queue| { with_queue(|queue| {
let params_data = [ let params_data = [
position.x, position.x,
position.z, position.z,
@@ -605,7 +603,7 @@ impl SnowLayer
queue.write_buffer(&self.deform_params_buffer, 0, params_bytes); queue.write_buffer(&self.deform_params_buffer, 0, params_bytes);
}); });
render::with_device(|device| { with_device(|device| {
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Snow Deform Encoder"), label: Some("Snow Deform Encoder"),
}); });
@@ -625,7 +623,7 @@ impl SnowLayer
compute_pass.dispatch_workgroups(dispatch_x, dispatch_y, 1); compute_pass.dispatch_workgroups(dispatch_x, dispatch_y, 1);
} }
render::with_queue(|queue| { with_queue(|queue| {
queue.submit(Some(encoder.finish())); queue.submit(Some(encoder.finish()));
}); });
}); });

View File

@@ -1,3 +1,4 @@
use super::{Spotlight, SpotlightRaw, MAX_SPOTLIGHTS};
use crate::paths; use crate::paths;
use bytemuck::{Pod, Zeroable}; use bytemuck::{Pod, Zeroable};
use glam::Vec2; use glam::Vec2;
@@ -13,12 +14,12 @@ struct AccumulationUniforms
delta_time: f32, delta_time: f32,
spotlight_count: u32, spotlight_count: u32,
_padding: u32, _padding: u32,
light_view_projections: [[[f32; 4]; 4]; crate::render::MAX_SPOTLIGHTS], light_view_projections: [[[f32; 4]; 4]; MAX_SPOTLIGHTS],
shadow_bias: f32, shadow_bias: f32,
terrain_height_scale: f32, terrain_height_scale: f32,
_padding3: f32, _padding3: f32,
_padding4: f32, _padding4: f32,
spotlights: [crate::render::SpotlightRaw; crate::render::MAX_SPOTLIGHTS], spotlights: [SpotlightRaw; MAX_SPOTLIGHTS],
} }
pub struct SnowLightAccumulation pub struct SnowLightAccumulation
@@ -417,7 +418,7 @@ impl SnowLightAccumulation
&mut self, &mut self,
encoder: &mut wgpu::CommandEncoder, encoder: &mut wgpu::CommandEncoder,
queue: &wgpu::Queue, queue: &wgpu::Queue,
spotlights: &[crate::render::Spotlight], spotlights: &[Spotlight],
delta_time: f32, delta_time: f32,
light_view_projections: &[glam::Mat4], light_view_projections: &[glam::Mat4],
shadow_bias: f32, shadow_bias: f32,
@@ -430,12 +431,8 @@ impl SnowLightAccumulation
self.needs_clear = false; self.needs_clear = false;
} }
let mut spotlight_array = let mut spotlight_array = [SpotlightRaw::default(); MAX_SPOTLIGHTS];
[crate::render::SpotlightRaw::default(); crate::render::MAX_SPOTLIGHTS]; for (i, spotlight) in spotlights.iter().take(MAX_SPOTLIGHTS).enumerate()
for (i, spotlight) in spotlights
.iter()
.take(crate::render::MAX_SPOTLIGHTS)
.enumerate()
{ {
spotlight_array[i] = spotlight.to_raw(); spotlight_array[i] = spotlight.to_raw();
} }
@@ -445,13 +442,13 @@ impl SnowLightAccumulation
terrain_max_xz: self.terrain_max.to_array(), terrain_max_xz: self.terrain_max.to_array(),
decay_rate: self.decay_rate, decay_rate: self.decay_rate,
delta_time, delta_time,
spotlight_count: spotlights.len().min(crate::render::MAX_SPOTLIGHTS) as u32, spotlight_count: spotlights.len().min(MAX_SPOTLIGHTS) as u32,
_padding: 0, _padding: 0,
light_view_projections: { light_view_projections: {
let mut arr = [[[0.0f32; 4]; 4]; crate::render::MAX_SPOTLIGHTS]; let mut arr = [[[0.0f32; 4]; 4]; MAX_SPOTLIGHTS];
for (i, mat) in light_view_projections for (i, mat) in light_view_projections
.iter() .iter()
.take(crate::render::MAX_SPOTLIGHTS) .take(MAX_SPOTLIGHTS)
.enumerate() .enumerate()
{ {
arr[i] = mat.to_cols_array_2d(); arr[i] = mat.to_cols_array_2d();

View File

@@ -1,13 +1,15 @@
use crate::snow::SnowLayer;
use crate::world::World; use crate::world::World;
pub fn snow_system(world: &World, snow_layer: &mut SnowLayer, noclip: bool) pub fn snow_system(world: &mut World, noclip: bool)
{ {
let camera_pos = world.active_camera_position(); let camera_pos = world.active_camera_position();
let player_pos = world.player_position(); let player_pos = world.player_position();
if let Some(ref mut snow_layer) = world.snow_layer
{
if !noclip if !noclip
{ {
snow_layer.deform_at_position(player_pos, 1.5, 10.0); snow_layer.deform_at_position(player_pos, 1.5, 10.0);
} }
snow_layer.update(camera_pos); snow_layer.update(camera_pos);
}
} }

View File

@@ -15,7 +15,9 @@ use crate::components::{
CameraComponent, InputComponent, JumpComponent, MeshComponent, MovementComponent, CameraComponent, InputComponent, JumpComponent, MeshComponent, MovementComponent,
PhysicsComponent, RotateComponent, PhysicsComponent, RotateComponent,
}; };
use crate::debug::DebugMode;
use crate::entity::{EntityHandle, EntityManager}; use crate::entity::{EntityHandle, EntityManager};
use crate::render::snow::SnowLayer;
use crate::state::StateMachine; use crate::state::StateMachine;
pub use crate::utility::transform::Transform; pub use crate::utility::transform::Transform;
@@ -107,6 +109,10 @@ pub struct World
pub bubble_tags: Storage<()>, pub bubble_tags: Storage<()>,
pub projectile_tags: Storage<()>, pub projectile_tags: Storage<()>,
pub dialog_outcomes: Vec<DialogOutcomeEvent>, pub dialog_outcomes: Vec<DialogOutcomeEvent>,
// --- singleton state (not per-entity) ---
pub snow_layer: Option<SnowLayer>,
pub debug_mode: DebugMode,
} }
impl World impl World
@@ -145,6 +151,8 @@ impl World
bubble_tags: Storage::new(), bubble_tags: Storage::new(),
projectile_tags: Storage::new(), projectile_tags: Storage::new(),
dialog_outcomes: Vec::new(), dialog_outcomes: Vec::new(),
snow_layer: None,
debug_mode: DebugMode::default(),
} }
} }