From bcbf30de148c12edbaea594b0209a85b064ba6b6 Mon Sep 17 00:00:00 2001 From: "Al @h0lybyte" <5599058+h0lybyte@users.noreply.github.com> Date: Sun, 8 Mar 2026 20:43:32 -0400 Subject: [PATCH 1/3] perf(isometric): reduce WASM chunk load radius and add frame-budgeted spawning (#7803) - cfg-gate LOAD_RADIUS: desktop=3, WASM=2 (25 vs 49 chunks) - Halve grass density on WASM to cut entity count - Rate-limit distant chunk spawns (1/frame WASM, 2 desktop) - Always spawn player's chunk + neighbors immediately for colliders - Clear spawn queue each frame to prevent duplicate accumulation --- .../isometric/src-tauri/src/game/terrain.rs | 9 +++++ .../isometric/src-tauri/src/game/tilemap.rs | 34 +++++++++++++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/apps/kbve/isometric/src-tauri/src/game/terrain.rs b/apps/kbve/isometric/src-tauri/src/game/terrain.rs index a31e3c211..77309f7ac 100644 --- a/apps/kbve/isometric/src-tauri/src/game/terrain.rs +++ b/apps/kbve/isometric/src-tauri/src/game/terrain.rs @@ -8,7 +8,12 @@ use super::player::Player; // --------------------------------------------------------------------------- pub const CHUNK_SIZE: i32 = 16; + +#[cfg(not(target_arch = "wasm32"))] pub const LOAD_RADIUS: i32 = 3; + +#[cfg(target_arch = "wasm32")] +pub const LOAD_RADIUS: i32 = 2; pub const MAX_HEIGHT: f32 = 6.0; pub const NOISE_SCALE: f32 = 6.0; pub const TERRAIN_SEED: u32 = 42; @@ -187,6 +192,10 @@ impl TerrainMap { pub fn update_around_player(&mut self, player_x: f32, player_z: f32) { let (pcx, pcz) = Self::tile_to_chunk(player_x.round() as i32, player_z.round() as i32); + // Rebuild spawn queue fresh each frame (avoids duplicates when + // rate-limited spawning leaves chunks un-spawned across frames). + self.chunks_to_spawn.clear(); + // Determine which chunks should be loaded let mut desired: Vec<(i32, i32)> = Vec::new(); for dx in -LOAD_RADIUS..=LOAD_RADIUS { diff --git a/apps/kbve/isometric/src-tauri/src/game/tilemap.rs b/apps/kbve/isometric/src-tauri/src/game/tilemap.rs index c1bb2f80c..1783abed2 100644 --- a/apps/kbve/isometric/src-tauri/src/game/tilemap.rs +++ b/apps/kbve/isometric/src-tauri/src/game/tilemap.rs @@ -8,6 +8,7 @@ use bevy::render::render_resource::{Extent3d, TextureDimension, TextureFormat}; use bevy_rapier3d::prelude::*; use super::grass::GrassTuft; +use super::player::Player; use super::terrain::{CHUNK_SIZE, TerrainMap, hash2d}; pub const TILE_SIZE: f32 = 1.0; @@ -361,12 +362,13 @@ fn process_chunk_spawns_and_despawns( mut meshes: ResMut>, mut terrain: ResMut, tile_materials: Option>, + player_query: Query<&Transform, With>, ) { let Some(tile_materials) = tile_materials else { return; }; - // Despawn chunks + // Despawn chunks (all at once — despawning is cheap) let despawns: Vec<(i32, i32, Vec)> = terrain.chunks_to_despawn.drain(..).collect(); for (_cx, _cz, entities) in despawns { for entity in entities { @@ -374,8 +376,31 @@ fn process_chunk_spawns_and_despawns( } } - // Spawn chunks - let spawns: Vec<(i32, i32)> = terrain.chunks_to_spawn.drain(..).collect(); + // Find the player's chunk so we can prioritize nearby chunks for colliders + let player_chunk = player_query.single().ok().map(|tf| { + TerrainMap::tile_to_chunk(tf.translation.x.round() as i32, tf.translation.z.round() as i32) + }); + + // Spawn chunks — always spawn player's chunk + neighbors immediately, + // rate-limit distant chunks to avoid frame spikes. + #[cfg(target_arch = "wasm32")] + const MAX_DISTANT_SPAWNS: usize = 1; + #[cfg(not(target_arch = "wasm32"))] + const MAX_DISTANT_SPAWNS: usize = 2; + + let mut near: Vec<(i32, i32)> = Vec::new(); + let mut far: Vec<(i32, i32)> = Vec::new(); + for (cx, cz) in terrain.chunks_to_spawn.drain(..) { + let is_near = player_chunk + .map(|(pcx, pcz)| (cx - pcx).abs() <= 1 && (cz - pcz).abs() <= 1) + .unwrap_or(true); + if is_near { near.push((cx, cz)); } else { far.push((cx, cz)); } + } + // Near chunks: always spawn (player needs colliders) + // Far chunks: rate-limit (terrain system re-queues un-spawned ones next frame) + far.truncate(MAX_DISTANT_SPAWNS); + let mut spawns = near; + spawns.extend(far); for (cx, cz) in &spawns { let mut entities = Vec::new(); let base_x = cx * CHUNK_SIZE; @@ -468,6 +493,9 @@ fn process_chunk_spawns_and_despawns( ]; for (seed_x, seed_z, density, kind) in grass_slots { + #[cfg(target_arch = "wasm32")] + let density = density * 0.5; + let noise = hash2d(tx + seed_x, tz + seed_z); if noise >= density { continue; From e6542434e669ac66289011f5abc3767c50ae4217 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 21:03:49 -0400 Subject: [PATCH 2/3] chore(ci): add KBVE Studio link to atomic PR body (#7805) Co-authored-by: Al @h0lybyte <5599058+h0lybyte@users.noreply.github.com> --- .github/workflows/ci-atom.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-atom.yml b/.github/workflows/ci-atom.yml index 5672559a3..03eff4418 100644 --- a/.github/workflows/ci-atom.yml +++ b/.github/workflows/ci-atom.yml @@ -244,7 +244,7 @@ jobs: - **Type:** Atomic workflow --- - *This PR was automatically created by the atomic branch workflow.*`; + *This PR was automatically created by the atomic branch workflow.* - [KBVE Studio](https://kbve.com/)`; try { const pr = await github.rest.pulls.create({ From 9e51323d3274b106898149c866c1e1c2d449f9e9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 22:16:01 -0400 Subject: [PATCH 3/3] chore(kube): update axum-kbve to v1.0.36 (#7806) Co-authored-by: github-actions[bot] --- apps/kube/kbve/manifest/kbve-deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/kube/kbve/manifest/kbve-deployment.yaml b/apps/kube/kbve/manifest/kbve-deployment.yaml index e62899d6b..1ae90608a 100644 --- a/apps/kube/kbve/manifest/kbve-deployment.yaml +++ b/apps/kube/kbve/manifest/kbve-deployment.yaml @@ -25,7 +25,7 @@ spec: spec: containers: - name: kbve - image: ghcr.io/kbve/kbve:1.0.35 + image: ghcr.io/kbve/kbve:1.0.36 imagePullPolicy: Always ports: - name: http