Skip to content
Open
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
39 changes: 29 additions & 10 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions pomme-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ tracing = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter"] }

tracing-appender = "0.2"
# Per-thread-heap allocator: the chunk-mesh worker pool churns vertex/index Vecs
# across threads, and the default system heap's global lock serializes that and
# stalls the main thread. Mirrors vanilla (JVM TLABs + LWJGL's jemalloc).
mimalloc = "0.1"
pomme-gpu-allocator = { path = "../pomme-gpu-allocator" }
winit = { version = "0.30", features = ["rwh_06"] }
raw-window-handle = "0.6"
Expand Down
1 change: 1 addition & 0 deletions pomme-client/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ fn main() {
let shaders = [
("chunk.vert", shaderc::ShaderKind::Vertex),
("chunk.frag", shaderc::ShaderKind::Fragment),
("chunk_solid.frag", shaderc::ShaderKind::Fragment),
("cube.vert", shaderc::ShaderKind::Vertex),
("cube.frag", shaderc::ShaderKind::Fragment),
("panorama.vert", shaderc::ShaderKind::Vertex),
Expand Down
11 changes: 11 additions & 0 deletions pomme-client/src/app/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,9 @@ impl AppCore {
) -> Option<String> {
let rx = &connection.event_rx;

// Phase timers for the chunk-load benchmark's worst-frame breakdown.
let t_net = std::time::Instant::now();

let mut chunks_to_mesh = Vec::new();
// Block edits go on the priority lane so they apply instantly even while
// chunks stream in, instead of starving behind the load backlog.
Expand Down Expand Up @@ -986,8 +989,16 @@ impl AppCore {
// then enqueue everything that needs meshing — visible-first, with hidden
// columns backfilled at a bounded rate so the world still completes.
let loads_happened = !chunks_to_mesh.is_empty();
let ms = |t: std::time::Instant| t.elapsed().as_secs_f32() * 1000.0;
game.last_update_phases.net_decode_ms = ms(t_net);

let t_vis = std::time::Instant::now();
game.update_visibility(renderer, player_chunk, loads_happened);
game.last_update_phases.visibility_ms = ms(t_vis);

let t_rescan = std::time::Instant::now();
game.rescan_mesh_jobs(player_chunk);
game.last_update_phases.rescan_ms = ms(t_rescan);

disconnect_reason
}
Expand Down
6 changes: 4 additions & 2 deletions pomme-client/src/app/phases/connecting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ pub fn update_connecting(
if matches!(connect_phase, ConnectionPhase::Loading) {
game.mesh_dispatcher
.set_camera_position(*game.player.position);
for mesh in game.mesh_dispatcher.drain_results() {
gfx.renderer.upload_chunk_mesh(&mesh);
let ready_meshes: Vec<_> = game.mesh_dispatcher.drain_results().collect();
gfx.renderer.upload_chunk_meshes(&ready_meshes);
for mesh in ready_meshes {
game.mesh_dispatcher.recycle(mesh);
}

let ready = game.position_set && (game.dead || gfx.renderer.loaded_chunk_count() > 0);
Expand Down
60 changes: 46 additions & 14 deletions pomme-client/src/app/phases/in_game.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ pub struct GameState {
/// In-flight/finished upload of the chunk-load result, while its overlay is
/// shown.
pub chunk_load_upload: Option<UploadHandle>,
/// Last frame's `update_game` CPU phase timings, for the chunk-load
/// benchmark's worst-frame breakdown.
pub last_update_phases: crate::benchmark::UpdatePhases,
/// Monotonic content generation per column, bumped on every edit (and chunk
/// load). This is the dirty marker: a column needs (re)meshing whenever its
/// `content_gen` outruns what was last enqueued, regardless of visibility,
Expand Down Expand Up @@ -199,6 +202,7 @@ impl GameState {
chunk_load_result: None,
chunk_load_abort: false,
chunk_load_upload: None,
last_update_phases: crate::benchmark::UpdatePhases::default(),
content_gen: HashMap::new(),
meshed: HashMap::new(),
vis_mask: HashMap::new(),
Expand Down Expand Up @@ -331,7 +335,6 @@ impl GameState {
let rd = self
.chunk_store
.loaded_positions()
.iter()
.map(|p| {
(p.x - player_chunk.x)
.abs()
Expand Down Expand Up @@ -596,6 +599,11 @@ pub fn update_game(
connection: &ConnectionHandle,
game: &mut GameState,
) -> GameUpdateResult {
// Snapshot last frame's phase timings before this frame overwrites them: they
// align with `raw_dt`, which measures the previous frame's full duration.
let frame_start = std::time::Instant::now();
let prev_phases = game.last_update_phases;

// Position the audio listener at the player's head and push current
// volumes before draining sound packets this frame.
let listener_pos = game.player.eye_pos();
Expand All @@ -611,7 +619,13 @@ pub fn update_game(
return GameUpdateResult::Disconnected { reason };
}

for mesh in game.mesh_dispatcher.drain_results() {
// Collect the frame's ready meshes, apply their CPU-side bookkeeping, then
// upload them in one coalesced GPU transfer (one fence wait, not one per
// mesh) to avoid the streaming stutter from per-mesh `queue.wait_idle`.
let drain_start = std::time::Instant::now();
let results: Vec<_> = game.mesh_dispatcher.drain_results().collect();
let mut batch = Vec::with_capacity(results.len());
for mut mesh in results {
// Drop a mesh built from an out-of-date snapshot. Edits (priority lane,
// single section) are keyed per section so editing one section never
// drops a sibling's in-flight result; bulk loads keep the column key.
Expand All @@ -623,6 +637,7 @@ pub fn update_game(
mesh.content_gen < game.content_gen.get(&mesh.pos).copied().unwrap_or(0)
};
if stale {
game.mesh_dispatcher.recycle(mesh);
continue;
}
if let Some(t) = &mesh.timing {
Expand All @@ -637,24 +652,34 @@ pub fn update_game(
ms(t.enqueued_at.elapsed()),
);
}
let dropped = gfx.renderer.upload_chunk_mesh(&mesh);
// Visibility updates are independent of the GPU upload; apply them now so
// the mesh can move into the upload batch.
let pos = mesh.pos;
// Sections dropped on pool exhaustion were retired from the buffer; clear
// their meshed bit so the next rescan re-enqueues them.
if !dropped.is_empty()
&& let Some(m) = game.meshed.get_mut(&pos)
{
for si in dropped {
m.mask &= !(1u32 << si);
}
}
for (si, vis) in mesh.visibility {
for (si, vis) in std::mem::take(&mut mesh.visibility) {
let e = game.section_vis_epoch.entry((pos, si)).or_insert(0);
if mesh.upload_epoch >= *e {
*e = mesh.upload_epoch;
game.section_vis.insert((pos, si), vis);
}
}
batch.push(mesh);
}
game.last_update_phases.mesh_drain_ms = drain_start.elapsed().as_secs_f32() * 1000.0;
let upload_start = std::time::Instant::now();
let dropped = gfx.renderer.upload_chunk_meshes(&batch);
game.last_update_phases.upload_ms = upload_start.elapsed().as_secs_f32() * 1000.0;
// Sections dropped on pool exhaustion were retired from the buffer; clear
// their meshed bit so the next rescan re-enqueues them.
for (pos, sections) in dropped {
if let Some(m) = game.meshed.get_mut(&pos) {
for si in sections {
m.mask &= !(1u32 << si);
}
}
}
// Return the uploaded meshes' buffers to the worker pool for reuse.
for mesh in batch {
game.mesh_dispatcher.recycle(mesh);
}

game.mesh_dispatcher
Expand Down Expand Up @@ -930,7 +955,12 @@ pub fn update_game(

if let Some(mut bench) = game.chunk_load_bench.take() {
let count = gfx.renderer.loaded_chunk_count();
match bench.update(count, raw_dt * 1000.0) {
match bench.update(
count,
raw_dt * 1000.0,
gfx.renderer.last_timings(),
prev_phases,
) {
ChunkLoadStep::Wait => {
game.chunk_load_bench = Some(bench);
}
Expand Down Expand Up @@ -1382,6 +1412,8 @@ pub fn update_game(
) {
tracing::error!("Render error: {e}");
}
// Whole-frame wall time (incl. render), read next frame to align with `raw_dt`.
game.last_update_phases.update_ms = frame_start.elapsed().as_secs_f32() * 1000.0;

if close_inventory {
game.inventory_open = false;
Expand Down
Loading
Loading