diff --git a/pomme-client/build.rs b/pomme-client/build.rs index 81716198e..bcd3781b1 100644 --- a/pomme-client/build.rs +++ b/pomme-client/build.rs @@ -5,6 +5,12 @@ use std::path::Path; // shaders via the generated packing.glsl. include!("src/renderer/chunk/packing_consts.rs"); +// The same for distant terrain's sizes and markers, shared via the generated +// distant_consts.glsl. +mod distant_consts { + include!("src/renderer/distant/shader_consts.rs"); +} + /// `PackedVertex::uv` back to a sprite-local UV, for `packing.glsl`. const UNPACK_SPRITE_UV: &str = "vec2 unpack_sprite_uv(uint uv) { uint mask = (1u << TERRAIN_UV_BITS) - 1u; @@ -52,6 +58,13 @@ fn main() { ) .expect("failed to write packing.glsl"); + println!("cargo:rerun-if-changed=src/renderer/distant/shader_consts.rs"); + fs::write( + Path::new(&out_dir).join("distant_consts.glsl"), + distant_consts_glsl(), + ) + .expect("failed to write distant_consts.glsl"); + let compiler = shaderc::Compiler::new().expect("failed to create shaderc compiler"); let mut options = shaderc::CompileOptions::new().expect("failed to create compile options"); options.set_target_env( @@ -84,6 +97,8 @@ fn main() { "item_instance.glsl", "lightmap.glsl", "light.glsl", + "distant_models.glsl", + "distant_section.glsl", ] { println!( "cargo:rerun-if-changed={}", @@ -96,6 +111,24 @@ fn main() { ("chunk.frag", shaderc::ShaderKind::Fragment), ("chunk_solid.frag", shaderc::ShaderKind::Fragment), ("water.frag", shaderc::ShaderKind::Fragment), + ("distant.vert", shaderc::ShaderKind::Vertex), + ("distant.frag", shaderc::ShaderKind::Fragment), + ("distant_bound.vert", shaderc::ShaderKind::Vertex), + ("distant_bound.frag", shaderc::ShaderKind::Fragment), + ("distant_setup.frag", shaderc::ShaderKind::Fragment), + ("distant_blit.frag", shaderc::ShaderKind::Fragment), + ("distant_cull.vert", shaderc::ShaderKind::Vertex), + ("distant_cull.frag", shaderc::ShaderKind::Fragment), + ("distant_scatter.comp", shaderc::ShaderKind::Compute), + ("distant_prep.comp", shaderc::ShaderKind::Compute), + ("distant_cmdgen.comp", shaderc::ShaderKind::Compute), + ("distant_prefix_sum.comp", shaderc::ShaderKind::Compute), + ("distant_translucents.comp", shaderc::ShaderKind::Compute), + ("distant_traversal.comp", shaderc::ShaderKind::Compute), + ("distant_hiz.frag", shaderc::ShaderKind::Fragment), + ("distant_cleaner_sort.comp", shaderc::ShaderKind::Compute), + ("distant_cleaner_results.comp", shaderc::ShaderKind::Compute), + ("distant_visibility_set.comp", shaderc::ShaderKind::Compute), ("cube.vert", shaderc::ShaderKind::Vertex), ("cube.frag", shaderc::ShaderKind::Fragment), ("panorama.vert", shaderc::ShaderKind::Vertex), @@ -170,6 +203,50 @@ fn main() { } } +/// `distant_consts.glsl`: the `uint` constants as `u` literals; plain those +/// that layouts or the preprocessor take. +fn distant_consts_glsl() -> String { + use distant_consts::*; + let uints = [ + ("LEVELS", LEVELS), + ("MAX_NODES", MAX_NODES), + ("NULL_ID", NULL_ID), + ("EMPTY_ID", EMPTY_ID), + ("CLEANER_SENTINEL", CLEANER_SENTINEL), + ("MAX_REQUESTS", MAX_REQUESTS), + ("MAX_LISTED", MAX_LISTED), + ("QUADS_PER_DRAW", QUADS_PER_DRAW), + ("OPAQUE_DRAWS", OPAQUE_DRAWS), + ("TRANSLUCENT_DRAWS", TRANSLUCENT_DRAWS), + ("TEMPORAL_DRAWS", TEMPORAL_DRAWS), + ("TRANSLUCENT_OFFSET", TRANSLUCENT_OFFSET), + ("TEMPORAL_OFFSET", TEMPORAL_OFFSET), + ("NODE_SCATTER_BIT", NODE_SCATTER_BIT), + ("VISIBILITY_RESET_BIT", VISIBILITY_RESET_BIT), + ("SEEN_BEFORE_BIT", SEEN_BEFORE_BIT), + ]; + let plain = [ + ("CLEANER_OUTPUT", u64::from(CLEANER_OUTPUT)), + ("TRANSLUCENT_BUCKETS", u64::from(TRANSLUCENT_BUCKETS)), + ("COUNTS_OPAQUE", COUNTS_OPAQUE), + ("COUNTS_TRANSLUCENT", COUNTS_TRANSLUCENT), + ("COUNTS_TEMPORAL", COUNTS_TEMPORAL), + ("COUNTS_CULL_DRAW", COUNTS_CULL_DRAW), + ("LIST_GROUP", u64::from(LIST_GROUP)), + ("TRAVERSAL_GROUP_BITS", u64::from(TRAVERSAL_GROUP_BITS)), + ("CLEANER_SORT_THREADS", u64::from(CLEANER_SORT_THREADS)), + ("CLEANER_SORT_ELEMS", u64::from(CLEANER_SORT_ELEMS)), + ]; + let mut glsl = String::from("#ifndef DISTANT_CONSTS_GLSL\n#define DISTANT_CONSTS_GLSL\n"); + for (name, value) in uints { + glsl += &format!("#define {name} {value}u\n"); + } + for (name, value) in plain { + glsl += &format!("#define {name} {value}\n"); + } + glsl + "#endif\n" +} + /// Deflates every block data table into `OUT_DIR/block-data/`, which /// `world::block` embeds: the JSON is mostly repeated shape and state data /// and compresses about twentyfold. diff --git a/pomme-client/src/app/core.rs b/pomme-client/src/app/core.rs index 62042b579..c39967005 100644 --- a/pomme-client/src/app/core.rs +++ b/pomme-client/src/app/core.rs @@ -30,6 +30,7 @@ use crate::player::LocalPlayer; use crate::player::tab_list::{PlayerChatValidation, TabList}; use crate::renderer::Renderer; use crate::renderer::chunk::mesh_source::MeshSource; +use crate::renderer::chunk::occlusion_graph::Update as VisUpdate; use crate::resource_pack::ResourcePackManager; use crate::ui::menu::{ CREDITS_KEY_CTRL_L, CREDITS_KEY_CTRL_R, CREDITS_KEY_SPACE, CREDITS_KEY_UP, MainMenu, MenuInput, @@ -143,6 +144,16 @@ enum InlineObjectContent { /// Vanilla `PlayerSkinRenderCache.CACHE_DURATION`. const GLYPH_CACHE_DURATION: std::time::Duration = std::time::Duration::from_secs(5 * 60); +/// How long a frame spends on chunk packets before the rest wait for the +/// next. +const CHUNK_PACKET_BUDGET: std::time::Duration = std::time::Duration::from_millis(3); + +/// Whether the rest of the queue may wait for the next frame: only once the +/// chunk budget is spent and while this frame owes no tick, so every packet +/// received before a tick is handled before it runs. +fn defer_packets(chunk_work: std::time::Duration, tick_accumulator: f32) -> bool { + chunk_work >= CHUNK_PACKET_BUDGET && tick_accumulator < crate::app::TICK_RATE +} /// Applies a server-driven block change: block-entity sync, prediction /// absorption, the block + light write, and the remesh cascade. The block @@ -207,6 +218,8 @@ fn write_block( pos.z, state, ); + game.edited_columns + .insert(crate::position::ChunkPos::containing(pos)); dirty_sections_for_block( priority_remesh, pos.x, @@ -423,6 +436,13 @@ fn hurt_entity(game: &mut GameState, id: i32, dir: Option) { /// The level's chunk, light and position updates, shared by the windowed /// client and the headless chunk benchmark; the caller does any renderer work. impl GameState { + /// The distant mesher tints with the chunk mesher's colours, so it takes + /// them again whenever the dispatcher or its biomes change. + fn sync_distant_colors(&mut self) { + self.distant + .set_colors(self.mesh_dispatcher.distant_colors()); + } + pub fn select_protocol(&mut self, protocol: i32) { let previous_slot = self.block_registry.block_state_table().table_slot(); self.block_registry.select_protocol(protocol); @@ -430,6 +450,44 @@ impl GameState { self.mesh_dispatcher = self .mesh_dispatcher .recreate_for_version(self.block_registry.owned_render_view()); + self.sync_distant_colors(); + self.set_distant_cache_key(); + self.respawn_light(self.light_engine.has_sky()); + } + } + + /// A fresh light engine for the chunk store's level, reading the current + /// block table. + fn respawn_light(&mut self, has_sky: bool) { + self.light_engine = crate::world::light::LightWorker::spawn( + self.chunk_store.height(), + self.chunk_store.min_y(), + has_sky, + self.block_registry.block_state_table(), + ); + } + + /// Distant terrain stores block states and biomes by id, so it keeps + /// them apart per block table and biome registry. + fn set_distant_cache_key(&mut self) { + if let Some(biomes) = crate::net::connection::biome_registry(&self.registries) { + self.distant.set_cache_key( + self.block_registry.block_state_table().table_slot(), + biomes.map.keys(), + ); + } + } + + /// Keeps `pos`'s column as distant terrain of the current dimension. + pub fn keep_column(&self, pos: crate::position::ChunkPos) { + self.distant + .capture(&self.dimension, pos, &self.chunk_store); + } + + /// Keeps the columns edited since the distant terrain took them. + pub fn keep_edited_columns(&mut self) { + for pos in std::mem::take(&mut self.edited_columns) { + self.keep_column(pos); } } @@ -441,6 +499,7 @@ impl GameState { self.biome_climate = Arc::new(colors); self.mesh_dispatcher .set_biome_climate(Arc::clone(&self.biome_climate)); + self.sync_distant_colors(); } /// Resets the level for a new dimension, with a mesh dispatcher for its @@ -473,8 +532,11 @@ impl GameState { // next tick samples the new level's. self.biome_weights = Default::default(); self.environment_probe = EnvironmentProbe::new(self.resolve_environment()); + // The old level's columns stay as its distant terrain; the name + // still names it until `DimensionName`. + self.keep_edited_columns(); self.chunk_store = ChunkStore::new_with_dimension(render_distance, height, min_y); - self.light_engine = crate::world::light::LevelLightEngine::new(height, min_y, has_skylight); + self.respawn_light(has_skylight); // The old level's block entities, moving blocks among them. self.block_entity_anim = Default::default(); self.position_set = false; @@ -489,8 +551,7 @@ impl GameState { // The meshes went with the old level, so their bookkeeping // has to go too, or the new level's camera section reads as // already meshed. - self.section_vis.clear(); - self.section_vis_epoch.clear(); + self.forget_visibility(VisUpdate::Clear); self.compiled.clear(); self.clear_visibility(); self.mesh_dispatcher = mesh_source.create_mesh_dispatcher( @@ -499,6 +560,7 @@ impl GameState { None, cardinal_light, ); + self.sync_distant_colors(); self.reset_mesh_columns(); } @@ -520,11 +582,10 @@ impl GameState { tracing::error!("Failed to load chunk [{}, {}]: {e}", pos.x, pos.z); return; } - self.light_engine.on_chunk_loaded( - &mut self.chunk_store, - self.block_registry.block_state_table(), - (pos.x, pos.z), - ); + if let Some(chunk) = self.chunk_store.get_chunk(&pos) { + self.light_engine.chunk_loaded((pos.x, pos.z), chunk); + } + self.walker.update(VisUpdate::Load(pos)); // The column meshes once its queued light applies (vanilla // schedules the rebuild from enableChunkLight, not here). queue_light_apply(self, pos, light, true); @@ -559,21 +620,21 @@ impl GameState { /// The caller removes the column's mesh. pub fn unload_chunk(&mut self, pos: crate::position::ChunkPos) { + self.keep_column(pos); + self.edited_columns.remove(&pos); self.chunk_store.unload_chunk(&pos); self.block_entity_anim.drop_chunk(pos.x, pos.z); - self.light_engine.on_chunk_unloaded((pos.x, pos.z)); - self.light_engine - .queue_task(crate::world::light::LightTask::Remove { - pos: (pos.x, pos.z), - }); + self.light_engine.chunk_unloaded((pos.x, pos.z)); self.forget_mesh_column(pos); self.compiled.remove(&pos); + let sections = self.chunk_store.section_count(); if let Some(mask) = self.in_view_masks.remove(&pos) { - let sections = self.chunk_store.section_count() as u32; - self.mesh_gate.remove(mask, sections); + self.mesh_gate.remove(mask, sections as u32); + } + for si in 0..sections { + self.section_vis_epoch.remove(&(pos, si)); } - self.section_vis.retain(|(p, _), _| *p != pos); - self.section_vis_epoch.retain(|(p, _), _| *p != pos); + self.walker.update(VisUpdate::Unload { pos, sections }); } /// Vanilla `handleMovePlayer`. Returns whether the player moved, for the @@ -630,11 +691,11 @@ impl GameState { correction.is_some() } + /// Vanilla `Options.setServerRenderDistance`, from the login and + /// `SetChunkCacheRadius`; clamped to the chunk grid. pub fn set_server_view_distance(&mut self, distance: u32) { tracing::info!("Server view distance: {distance}"); - if let Some(d) = server_view_distance_update(distance, self.last_render_distance) { - self.server_render_distance = d; - } + self.server_render_distance = distance.min(crate::world::chunk::MAX_VIEW_DISTANCE); } } @@ -925,6 +986,9 @@ pub struct AppCore { pub audio: crate::audio::AudioEngine, pub tick_accumulator: f32, pub time_tick_accumulator: f32, + /// Ticks the loading screen handed to the game phase: the join tick, which + /// sent `player_loaded`, and the rest of its frame's. + pub carried_ticks: u32, menu_dpad: RepeatStepper, /// When the window lost OS focus, for pause-on-lost-focus (vanilla /// `pauseIfInactive`); `None` while focused. @@ -1097,6 +1161,7 @@ impl AppCore { audio, tick_accumulator: 0.0, time_tick_accumulator: 0.0, + carried_ticks: 0, menu_dpad: RepeatStepper::default(), unfocused_since: None, deferred_quick_play: None, @@ -1247,6 +1312,7 @@ impl AppCore { self.menu.restore_bench_render_distance(); self.clear_server_resource_packs(&mut gfx.renderer); gfx.renderer.clear_chunk_meshes(); + gfx.renderer.reset_distant(); if let Some(p) = &mut self.presence { p.set_in_menu(&self.version); } @@ -1676,6 +1742,7 @@ impl AppCore { ); let (grass, foliage, dry_foliage) = mesh_dispatcher.colormaps(); game.mesh_dispatcher = mesh_dispatcher; + game.sync_distant_colors(); game.particle_store = crate::particle::ParticleStore::new( renderer.uv_map().clone(), grass, @@ -1776,9 +1843,26 @@ impl AppCore { let mut disconnect_reason: Option = None; self.drain_player_skin_results(renderer); - // Vanilla `PacketProcessor.processQueuedPackets`: drain everything - // before this frame's ticks. - while let Ok(event) = rx.try_recv() { + // Vanilla `PacketProcessor.processQueuedPackets` drains everything + // before this frame's ticks. A departure: after `CHUNK_PACKET_BUDGET` + // of chunk packets the rest wait, in order, for the next frame, so a + // burst of chunks is a few short frames instead of one long one. Only + // frames that owe no tick split it (`defer_packets`; callers add the + // frame's time first), so they render and nothing more. + // `ChunkBatchReceived` is answered on the network thread as vanilla + // does, so this doesn't pace the server. + let mut chunk_work = std::time::Duration::ZERO; + while !defer_packets(chunk_work, self.tick_accumulator) + && let Ok(event) = rx.try_recv() + { + let chunk_packet = matches!( + event, + NetworkEvent::ChunkLoaded { .. } + | NetworkEvent::ChunkUnloaded { .. } + | NetworkEvent::LightUpdate { .. } + | NetworkEvent::ChunkBiomes { .. } + ) + .then(std::time::Instant::now); match event { NetworkEvent::ProtocolSelected { protocol } => game.select_protocol(protocol), NetworkEvent::Connected => { @@ -1786,6 +1870,7 @@ impl AppCore { tracing::info!("Connected to server"); // Ticks restart from zero with the play state. self.tick_accumulator = 0.0; + self.carried_ticks = 0; *state = ConnectionPhase::Loading; } else { tracing::warn!("Unexpected NetworkEvent::Connected, skipping"); @@ -1800,6 +1885,7 @@ impl AppCore { NetworkEvent::DimensionInfo(dim) => { game.enter_dimension(dim, self.menu.render_distance, &*renderer); renderer.clear_chunk_meshes(); + renderer.reset_distant(); } NetworkEvent::DimensionName { name } => { game.dimension = name; @@ -1809,12 +1895,15 @@ impl AppCore { data, heightmaps, light, - } => game.load_chunk(pos, &data, &heightmaps, &light), + } => { + game.load_chunk(pos, &data, &heightmaps, &light); + } NetworkEvent::LightUpdate { pos, light } => game.apply_light_update(pos, &light), NetworkEvent::ChunkBiomes { chunks } => game.replace_biomes(&chunks), NetworkEvent::ChunkUnloaded { pos } => { game.unload_chunk(pos); renderer.remove_chunk_mesh(&pos); + renderer.set_distant_column_sections(pos, 0, game.chunk_store.min_section_y()); } NetworkEvent::ChunkCacheCenter { x, z } => { tracing::debug!("Chunk cache center: [{x}, {z}]"); @@ -2008,6 +2097,7 @@ impl AppCore { } NetworkEvent::Registries(registries) => { game.registries = registries; + game.set_distant_cache_key(); } NetworkEvent::DialogRegistry(registry) => { game.dialog_registry = registry; @@ -2559,13 +2649,7 @@ impl AppCore { let min_y = game.chunk_store.min_y(); let n = game.chunk_store.section_count(); for b in ack_dirty { - game.light_engine.on_block_dirty( - &game.chunk_store, - game.block_registry.block_state_table(), - b.x, - b.y, - b.z, - ); + game.light_engine.block_dirty(b.x, b.y, b.z); dirty_sections_for_block(&mut priority_remesh, b.x, b.y, b.z, min_y, n); } } @@ -3121,6 +3205,9 @@ impl AppCore { game.tab_list.set_header_footer(header, footer); } } + if let Some(started) = chunk_packet { + chunk_work += started.elapsed(); + } } if let Some(pending) = &self.pending_pack_download @@ -3672,13 +3759,7 @@ impl AppCore { // Light lands in this frame's update_light, matching vanilla's // prediction timing (setBlockState queues; the per-frame // ClientLevel.update drains). - game.light_engine.on_block_dirty( - &game.chunk_store, - game.block_registry.block_state_table(), - b.x, - b.y, - b.z, - ); + game.light_engine.block_dirty(b.x, b.y, b.z); dirty_sections_for_block(&mut sections, b.x, b.y, b.z, min_y, n); } // One span per column so each column builds its mesh snapshot @@ -3952,17 +4033,6 @@ fn sync_player_attribute_mirrors(player: &mut LocalPlayer, dirty: impl Fn(Attrib } } -/// New `server_render_distance` for a server view-distance announcement, or -/// `None` to keep the current one. Some servers announce min(our request, -/// server max); an echo of our own request carries no cap information and -/// would ratchet the render distance slider down, so only a differing value -/// counts. It can't be an echo above the request: any such value is the -/// server's actual view distance, including later reductions. -fn server_view_distance_update(announced: u32, last_request: u32) -> Option { - let announced = announced.min(crate::world::chunk::MAX_VIEW_DISTANCE); - (announced != last_request).then_some(announced) -} - /// LOD level for a column: full detail within `detail` chunks (the Chunk /// Detail setting), half resolution to twice that, quarter beyond. Off keeps /// every column at full detail. @@ -4044,11 +4114,11 @@ fn compute_fov_modifier(player: &LocalPlayer, effect_scale: f32) -> f32 { #[cfg(test)] mod tests { use super::{ - CursorOp, DeathRoute, HeadProfile, MENU_REPEAT_DELAY, MENU_REPEAT_INTERVAL, RepeatStepper, - accepted_player_chat_tag, cursor_step, death_route, player_input_state, - resolve_corrected_look, resolve_head_profile, resolve_player_position_correction, - server_view_distance_update, serverbound_player_input, sync_living_attribute_mirrors, - sync_player_attribute_mirrors, sync_respawn_attributes, + CHUNK_PACKET_BUDGET, CursorOp, DeathRoute, HeadProfile, MENU_REPEAT_DELAY, + MENU_REPEAT_INTERVAL, RepeatStepper, accepted_player_chat_tag, cursor_step, death_route, + player_input_state, resolve_corrected_look, resolve_head_profile, + resolve_player_position_correction, serverbound_player_input, + sync_living_attribute_mirrors, sync_player_attribute_mirrors, sync_respawn_attributes, }; use crate::app::input::{InputState, gamepad_movement_axes}; use crate::attribute::{ @@ -4083,6 +4153,16 @@ mod tests { tab_list } + #[test] + fn packets_wait_only_on_a_frame_that_owes_no_tick() { + use crate::app::TICK_RATE; + let spent = CHUNK_PACKET_BUDGET; + assert!(!super::defer_packets(spent / 2, 0.0)); + assert!(super::defer_packets(spent, 0.9 * TICK_RATE)); + assert!(!super::defer_packets(spent, TICK_RATE)); + assert!(!super::defer_packets(spent * 10, 3.0 * TICK_RATE)); + } + #[test] fn player_names_follow_the_codec_rules() { assert!(valid_player_name("Dinnerbone")); @@ -4387,20 +4467,6 @@ mod tests { assert!(!released.sprint); } - #[test] - fn server_view_distance_updates() { - // Echo of our own request carries no cap information. - assert_eq!(server_view_distance_update(12, 12), None); - // Below the request: min(request, cap) revealed the server cap. - assert_eq!(server_view_distance_update(10, 12), Some(10)); - // Above the request: the server's actual view distance, including a - // reduction from an earlier higher announcement. - assert_eq!(server_view_distance_update(64, 12), Some(64)); - assert_eq!(server_view_distance_update(20, 12), Some(20)); - // Wire values past the chunk grid's extent clamp to it. - assert_eq!(server_view_distance_update(300, 12), Some(128)); - } - #[test] fn max_health_updates_clamp_current_health() { let mut attributes = AttributeMap::player(); diff --git a/pomme-client/src/app/mod.rs b/pomme-client/src/app/mod.rs index fedcb6f21..509118182 100644 --- a/pomme-client/src/app/mod.rs +++ b/pomme-client/src/app/mod.rs @@ -28,7 +28,7 @@ use crate::app::phases::saving::update_saving; use crate::app::phases::{AfterSaving, AppPhase, ConnectionPhase, FpsCounter, Gfx, Panorama}; use crate::app::state_slot::StateSlot; use crate::dirs::DataDirs; -use crate::net::connection::{ConnectArgs, ConnectionHandle, spawn_connection}; +use crate::net::connection::{ConnectArgs, ConnectionHandle, Transport, spawn_connection}; use crate::renderer::{self, Renderer}; use crate::singleplayer::World; use crate::user::UserData; @@ -153,6 +153,7 @@ fn leave_world( drop(connection); core.audio.stop_all_sounds(); core.return_to_menu(&mut gfx); + core.carried_ticks = 0; match world { Some(world) => { @@ -194,6 +195,18 @@ fn start_connecting( // a different session protocol. The registry receives explicit metadata. let protocol = crate::version::selected_protocol(); let block_definitions = Rc::clone(&core.block_definitions); + // A world keeps its distant terrain in its own folder; a server's goes + // under the game folder. + let distant_cache = match (&world, &connect_args.transport) { + (Some(world), _) => Some(world.dir().join("distant_terrain")), + (None, Transport::Remote { server, .. }) => Some( + core.data_dirs + .game_dir + .join("distant_terrain") + .join(crate::renderer::distant::worker::folder_name(server)), + ), + (None, Transport::Memory { .. }) => None, + }; let connection = spawn_connection(&core.tokio_rt, connect_args); let game = GameState::new( &*gfx.renderer, @@ -204,6 +217,7 @@ fn start_connecting( core.menu.skin_options(), block_definitions, protocol, + distant_cache, ); core.apply_cursor_grab(&gfx.window, None); AppPhase::Connecting { @@ -487,6 +501,12 @@ impl ApplicationHandler for App { if let Some(install) = &mut self.install { install.suspend(); } + // Android may kill a suspended app without dropping anything. + if let AppPhase::Connecting { game, .. } | AppPhase::InGame { game, .. } = + self.phase.get_mut() + { + game.distant.flush(); + } if let Some(gfx) = self.phase.gfx_mut() { gfx.renderer.suspend(); } diff --git a/pomme-client/src/app/phases/connecting.rs b/pomme-client/src/app/phases/connecting.rs index 89cdaa293..75e5dbbd4 100644 --- a/pomme-client/src/app/phases/connecting.rs +++ b/pomme-client/src/app/phases/connecting.rs @@ -1,4 +1,3 @@ -use crate::app::TICK_RATE; use crate::app::core::AppCore; use crate::app::phases::in_game::{GameState, build_server_screens}; use crate::app::phases::{ConnectionPhase, Gfx, Panorama, draw_status}; @@ -37,6 +36,10 @@ pub fn update_connecting( return ConnectingUpdateResult::Disconnected { reason }; } + // Added before the drain, which takes everything once a tick is owed. + if matches!(connect_phase, ConnectionPhase::Loading) && game.level_load.is_some() { + core.tick_accumulator += dt; + } let disconnect_reason = core.drain_network_events( connection, Some(connect_phase), @@ -59,18 +62,18 @@ pub fn update_connecting( // `LocalPlayer.tick` stays parked. Those ticks need a level, which // arrives with the login that also starts the tracker. if game.level_load.is_some() { - core.tick_accumulator += dt; - while core.tick_accumulator >= TICK_RATE { + let ticks = super::in_game::ticks_to_run(&mut core.tick_accumulator); + for done in 0..ticks { AppCore::tick_level_load(gfx.renderer.camera_render_position(), connection, game); if game.client_loaded { // Vanilla closes `LevelLoadingScreen` on the same tick that // sends `player_loaded`, and the local player then ticks - // (and moves) later in it. Hand this tick to the game phase - // unspent so it plays out there. + // (and moves) later in it. Hand this tick and the rest to + // the game phase, which runs them before handling packets. + core.carried_ticks = ticks - done; return ConnectingUpdateResult::JoinGame; } AppCore::send_client_tick_end(connection); - core.tick_accumulator -= TICK_RATE; } } else { core.tick_accumulator = 0.0; diff --git a/pomme-client/src/app/phases/in_game.rs b/pomme-client/src/app/phases/in_game.rs index fd377e7d9..c53783c82 100644 --- a/pomme-client/src/app/phases/in_game.rs +++ b/pomme-client/src/app/phases/in_game.rs @@ -32,11 +32,12 @@ use crate::player::menu_click::ContainerKind; use crate::player::tab_list::TabList; use crate::position::{BlockPos, ChunkPos}; use crate::renderer::chunk::block_lighter::{self, LightLevels, WorldLight}; -use crate::renderer::chunk::buffer::{column_is_near, section_bit}; +use crate::renderer::chunk::buffer::section_bit; use crate::renderer::chunk::mesh_columns::MeshColumns; use crate::renderer::chunk::mesh_source::MeshSource; use crate::renderer::chunk::mesher::{BiomeClimate, ChunkMeshData, MeshDispatcher}; -use crate::renderer::chunk::occlusion_graph::{self, VisibilitySet}; +use crate::renderer::chunk::occlusion_graph::{self, section_mask}; +use crate::renderer::distant::gpu::View as DistantView; use crate::renderer::entity_model::{lerp, triangle_wave}; use crate::renderer::pipelines::block_entity; use crate::renderer::pipelines::entity_renderer::{ @@ -156,9 +157,10 @@ impl OpenContainer { pub struct GameState { pub chunk_store: ChunkStore, - /// Client-side light engine (vanilla `LevelLightEngine`); recreated with - /// the chunk store on dimension changes, drained once per tick. - pub light_engine: crate::world::light::LevelLightEngine, + /// The light thread (vanilla `LevelLightEngine`, run off the game + /// thread); recreated with the chunk store on dimension changes, polled + /// once per frame. + pub light_engine: crate::world::light::LightWorker, /// Set by [`Self::update_light`] when chunk-load light marked columns /// dirty; consumed by the visibility refresh as its new-loads signal. pub pending_load_rescan: bool, @@ -186,6 +188,11 @@ pub struct GameState { pub player_walk_speed: f32, pub player_prev_walk_speed: f32, pub mesh_dispatcher: MeshDispatcher, + /// Every chunk seen, kept to draw past the loaded ones. + pub(crate) distant: crate::renderer::distant::worker::DistantTerrain, + /// Columns with blocks changed since `distant` took them, taken again + /// on the way out. + pub edited_columns: HashSet, /// Whether the server is this process, which the pause menu labels. pub singleplayer: bool, pub paused: bool, @@ -370,34 +377,46 @@ pub struct GameState { pub last_update_phases: crate::benchmark::UpdatePhases, /// Which loaded columns need (re)meshing. mesh_columns: MeshColumns, + /// Meshes that didn't fit the frame's staging buffer, sent first next + /// frame. + pending_upload: Vec, /// The last walk's in-frustum columns and their visible-section masks, so /// an unloading column can leave `mesh_gate`. pub in_view_masks: HashMap, - /// Per-section cave-cull visibility (vanilla `VisibilitySet`). Fed by mesh - /// results; consumed by the occlusion walk. - pub section_vis: HashMap<(ChunkPos, i32), VisibilitySet>, + /// The occlusion walk thread, holding every section's cave-cull + /// visibility (vanilla `VisibilitySet`) as mesh results report it. + pub walker: occlusion_graph::Walker, /// Per-column bitmask of sections whose mesh is finished and, if it had /// any geometry, uploaded — vanilla's "not `UNCOMPILED`", where an empty /// mesh counts too. The level load gate waits on the camera's bit. pub compiled: HashMap, - /// Highest upload epoch each `section_vis` entry was set from; mirrors the - /// buffer's per-section geometry gate so a stale bulk can't re-stale an - /// edited section's visibility. + /// Highest upload epoch each section's visibility was sent from; mirrors + /// the buffer's per-section geometry gate so a stale bulk can't re-stale + /// an edited section's visibility. pub section_vis_epoch: HashMap<(ChunkPos, i32), u64>, pub mesh_gate: MeshGate, pub vis_valid: bool, /// Camera 8-block bucket that last triggered an occlusion walk — movement, /// not rotation, drives recomputes (vanilla's cadence). pub last_vis_cam: (i32, i32, i32), - /// In-flight async occlusion walk; its result is applied a few frames - /// later. - pub vis_task: Option>>, + /// Whether a walk is in flight; its result is applied a few frames later. + pub vis_walk_pending: bool, + /// Bumped when the level is cleared, so an older walk's result is dropped. + pub vis_generation: u64, /// Runtime toggle for graph-driven chunk occlusion culling (F3+O). When /// off, only frustum culling applies (full masks pushed to the /// renderer). pub chunk_occlusion_enabled: bool, } +impl Drop for GameState { + /// The edited columns stay as distant terrain, written to disk as + /// `distant` drops. + fn drop(&mut self) { + self.keep_edited_columns(); + } +} + impl GameState { /// The day time is the dimension's default clock. pub fn sync_day_time(&mut self) { @@ -480,6 +499,7 @@ impl GameState { skin_options: crate::ui::menu::SkinOptions, block_definitions: Rc, protocol: i32, + distant_cache: Option, ) -> Self { let block_registry = BlockRegistry::new( block_definitions, @@ -497,11 +517,16 @@ impl GameState { ); let chunk_store = ChunkStore::new(render_distance); + let distant = crate::renderer::distant::worker::DistantTerrain::new( + mesh_dispatcher.distant_colors(), + distant_cache, + ); Self { - light_engine: crate::world::light::LevelLightEngine::new( + light_engine: crate::world::light::LightWorker::spawn( chunk_store.height(), chunk_store.min_y(), true, + block_registry.block_state_table(), ), pending_load_rescan: false, chunk_store, @@ -538,6 +563,8 @@ impl GameState { player_walk_speed: 0.0, player_prev_walk_speed: 0.0, mesh_dispatcher, + distant, + edited_columns: HashSet::new(), singleplayer, paused: false, dead: false, @@ -641,14 +668,16 @@ impl GameState { chunk_load_upload: None, last_update_phases: crate::benchmark::UpdatePhases::default(), mesh_columns: MeshColumns::default(), + pending_upload: Vec::new(), in_view_masks: HashMap::new(), - section_vis: HashMap::new(), + walker: occlusion_graph::Walker::spawn(), compiled: HashMap::new(), section_vis_epoch: HashMap::new(), mesh_gate: MeshGate::default(), vis_valid: false, last_vis_cam: (i32::MIN, i32::MIN, i32::MIN), - vis_task: None, + vis_walk_pending: false, + vis_generation: 0, chunk_occlusion_enabled: true, } } @@ -1239,12 +1268,20 @@ impl GameState { pub fn rebuild_meshes(&mut self) { self.reset_mesh_columns(); self.remesh_all_columns(); - self.section_vis.clear(); + self.pending_upload.clear(); + self.forget_visibility(occlusion_graph::Update::ClearSections); + self.pending_load_rescan = true; + } + + /// Drops every section's visibility and the last walk's result, as when + /// the meshes go; `clear` says whether the walker keeps the columns. + pub fn forget_visibility(&mut self, clear: occlusion_graph::Update) { + self.walker.update(clear); + self.vis_generation += 1; + self.vis_walk_pending = false; self.section_vis_epoch.clear(); self.clear_visibility(); - self.vis_task = None; self.vis_valid = false; - self.pending_load_rescan = true; } /// Marks every loaded column for remeshing (F3+A). @@ -1270,16 +1307,13 @@ impl GameState { /// loads, individual lit sections remesh as edits. pub fn update_light(&mut self) { let mut dirty = crate::world::light::LightDirty::default(); - self.light_engine.poll_and_run( - &mut self.chunk_store, - self.block_registry.block_state_table(), - &mut dirty, - ); + self.light_engine.poll(&mut self.chunk_store, &mut dirty); if dirty.columns.is_empty() && dirty.sections.is_empty() { return; } let mut bumped: Vec = Vec::new(); for &(x, z) in &dirty.columns { + self.keep_column(ChunkPos::new(x, z)); for p in crate::world::chunk::mesh_neighborhood(ChunkPos::new(x, z)) { if self.chunk_store.get_chunk(&p).is_some() && !bumped.contains(&p) { bumped.push(p); @@ -1292,7 +1326,7 @@ impl GameState { if !bumped.is_empty() { self.pending_load_rescan = true; } - let min_section_y = self.chunk_store.min_y() >> 4; + let min_section_y = self.chunk_store.min_section_y(); let section_count = self.chunk_store.section_count(); for key in &dirty.sections { let si = key.y - min_section_y; @@ -1334,57 +1368,55 @@ impl GameState { self.apply_mesh_upload(renderer, mesh); } - /// 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`. - /// Shared with the loading phase, which streams the spawn chunks in before - /// the game phase takes over. + /// Collect the frame's ready meshes, upload as many as fit the frame's + /// staging buffer in one coalesced GPU transfer (one fence wait, not one + /// per mesh) and apply their CPU-side bookkeeping; the rest go first next + /// frame. Shared with the loading phase, which streams the spawn chunks + /// in before the game phase takes over. pub fn drain_and_upload_meshes(&mut self, renderer: &mut Renderer) { - let batch = self.drain_meshes(); + let mut batch = self.drain_meshes(); let upload_start = std::time::Instant::now(); + renderer.upload_distant(self.distant.drain()); + let (wanted, gone) = renderer.take_distant_requests(); + self.distant.request(wanted, gone); let uploads = renderer.upload_chunk_meshes(&batch); + self.pending_upload = batch.split_off(uploads.taken); + for mesh in &mut batch { + self.apply_mesh_bookkeeping(mesh); + } self.last_update_phases.upload_ms = upload_start.elapsed().as_secs_f32() * 1000.0; self.apply_uploads(uploads); + // Distant terrain gives way once a column's sections draw, not once + // they load: the ring loaded around the view waits on its + // neighbours to compile. + let min_section = self.chunk_store.min_section_y(); + for mesh in &batch { + let sections = self.compiled.get(&mesh.pos).copied().unwrap_or(0); + renderer.set_distant_column_sections(mesh.pos, sections, min_section); + } } - /// The frame's ready meshes, stale ones dropped and the rest's CPU-side - /// bookkeeping applied. + /// The meshes still waiting to upload and then the frame's ready ones, + /// stale ones dropped. pub fn drain_meshes(&mut self) -> Vec { let drain_start = std::time::Instant::now(); - let results: Vec<_> = self.mesh_dispatcher.drain_results().collect(); - let mut batch = Vec::with_capacity(results.len()); - for mut mesh in results { + let mut batch = std::mem::take(&mut self.pending_upload); + for mesh in self.mesh_dispatcher.drain_results() { // Stale meshes count too: worker time spent is worker time spent. if let Some(bench) = &mut self.chunk_load_bench { bench.record_mesh(mesh.queue_ms, mesh.mesh_ms); } - // Drop a mesh for a load of the chunk that has since unloaded - // (uploading it would resurrect a column nothing cleans up). One - // from an older generation still uploads: the buffer's epoch gate - // keeps it from replacing anything newer, and its sections stay - // dirty for the newer job. - let stale = self.chunk_store.get_chunk(&mesh.pos).is_none() - || self.mesh_columns.incarnation(mesh.pos) != Some(mesh.incarnation); - if stale { - continue; - } - if let Some(t) = &mesh.timing { - let ms = |d: std::time::Duration| d.as_secs_f32() * 1000.0; - tracing::debug!( - "edit remesh [{}, {}]: queue {:.1}ms + mesh {:.1}ms + drain {:.1}ms = {:.1}ms", - mesh.pos.x, - mesh.pos.z, - ms(t.started_at - t.enqueued_at), - ms(t.meshed_at - t.started_at), - ms(t.meshed_at.elapsed()), - ms(t.enqueued_at.elapsed()), - ); - } - // Taken before the upload so the mesh can move into the batch; the - // upload reports back what it had to drop. - self.apply_mesh_bookkeeping(&mut mesh); batch.push(mesh); } + // Drop a mesh for a load of the chunk that has since unloaded + // (uploading it would resurrect a column nothing cleans up). One + // from an older generation still uploads: the buffer's epoch gate + // keeps it from replacing anything newer, and its sections stay + // dirty for the newer job. + batch.retain(|mesh| { + self.chunk_store.get_chunk(&mesh.pos).is_some() + && self.mesh_columns.incarnation(mesh.pos) == Some(mesh.incarnation) + }); self.last_update_phases.mesh_drain_ms = drain_start.elapsed().as_secs_f32() * 1000.0; batch } @@ -1436,13 +1468,26 @@ impl GameState { /// epoch-guarded so a stale result can't overwrite a newer edit's /// visibility, and the sections it compiled. The upload can still starve /// a section afterwards, which `apply_uploads` takes back out. - fn apply_mesh_bookkeeping(&mut self, mesh: &mut ChunkMeshData) { + pub(crate) fn apply_mesh_bookkeeping(&mut self, mesh: &mut ChunkMeshData) { + if let Some(t) = &mesh.timing { + let ms = |d: std::time::Duration| d.as_secs_f32() * 1000.0; + tracing::debug!( + "edit remesh [{}, {}]: queue {:.1}ms + mesh {:.1}ms + drain {:.1}ms = {:.1}ms", + mesh.pos.x, + mesh.pos.z, + ms(t.started_at - t.enqueued_at), + ms(t.meshed_at - t.started_at), + ms(t.meshed_at.elapsed()), + ms(t.enqueued_at.elapsed()), + ); + } let pos = mesh.pos; for (si, vis) in std::mem::take(&mut mesh.visibility) { let e = self.section_vis_epoch.entry((pos, si)).or_insert(0); if mesh.upload_epoch >= *e { *e = mesh.upload_epoch; - self.section_vis.insert((pos, si), vis); + self.walker + .update(occlusion_graph::Update::Section { pos, si, vis }); } } let meshed = section_bits(mesh.replaced.clone()); @@ -1500,10 +1545,18 @@ impl GameState { } // Apply a finished walk (its result lags a few frames, like vanilla's). - let finished = self.vis_task.as_ref().and_then(|rx| rx.try_recv().ok()); - if let Some(bfs) = finished { - self.vis_task = None; - self.apply_visibility(renderer, &bfs); + if let Some(result) = self.walker.try_result() + && result.generation == self.vis_generation + { + self.vis_walk_pending = false; + let sections = self.chunk_store.section_count() as u32; + self.mesh_gate = MeshGate::default(); + for &mask in result.in_view.values() { + self.mesh_gate.add(mask, sections); + } + self.in_view_masks = result.in_view; + self.vis_valid = true; + renderer.set_chunk_visibility(result.draw); } // Schedule the next walk on 8-block movement, chunk loads, or an @@ -1515,40 +1568,21 @@ impl GameState { (eye.y / 8.0).floor() as i32, (eye.z / 8.0).floor() as i32, ); - if self.vis_task.is_none() + if !self.vis_walk_pending && (!self.vis_valid || cam_bucket != self.last_vis_cam || loads_happened) { self.last_vis_cam = cam_bucket; - let section_vis = self.section_vis.clone(); - let min_y = self.chunk_store.min_y(); - let n = self.chunk_store.section_count(); - let cam_si = ((eye.y - min_y as f64) / 16.0).floor() as i32; - // Bound the walk by the actual loaded radius (a server can stream - // terrain past the client render distance). - let rd = self - .chunk_store - .loaded_positions() - .map(|p| { - (p.x - player_chunk.x) - .abs() - .max((p.z - player_chunk.z).abs()) - }) - .max() - .unwrap_or(0); - let (tx, rx) = crossbeam_channel::bounded(1); - std::thread::spawn(move || { - let bfs = occlusion_graph::compute_visible_mask( - §ion_vis, - player_chunk, - cam_si, - eye, - min_y, - n, - rd, - ); - let _ = tx.send(bfs); + self.vis_walk_pending = true; + self.walker.walk(occlusion_graph::Walk { + generation: self.vis_generation, + cam_col: player_chunk, + eye, + planes: renderer.frustum_planes(), + planes_wide: renderer.frustum_planes_dilated(occlusion_graph::VIS_MARGIN_RADIANS), + min_y: self.chunk_store.min_y(), + section_count: self.chunk_store.section_count(), + occlusion: self.chunk_occlusion_enabled, }); - self.vis_task = Some(rx); } } @@ -1558,51 +1592,6 @@ impl GameState { self.mesh_gate = MeshGate::default(); } - /// Combine a finished walk with the current camera frustum into per-column - /// draw masks (occluded sections omitted), and push them to the GPU cull. - fn apply_visibility(&mut self, renderer: &mut Renderer, bfs: &HashMap) { - let planes = renderer.frustum_planes(); - let planes_wide = renderer.frustum_planes_dilated(VIS_MARGIN_RADIANS); - let eye_f = renderer.camera_render_position(); - let min_y = self.chunk_store.min_y() as f32; - let max_y = min_y + self.chunk_store.height() as f32; - let full = section_mask(self.chunk_store.section_count()); - let sections = self.chunk_store.section_count() as u32; - - let mut masks = HashMap::new(); - let mut in_view = HashMap::new(); - let mut gate = MeshGate::default(); - for pos in self.chunk_store.loaded_positions() { - let near = column_is_near(pos, eye_f); - // Near columns always draw fully; otherwise a column draws only the - // sections the graph proved occlusion-visible (none => fully hidden). - let mask = if near { - full - } else { - bfs.get(&pos).copied().unwrap_or(0) - }; - if mask != 0 - && (near - || column_frustum_tier(pos, eye_f, &planes, &planes_wide, min_y, max_y) == 0) - { - gate.add(mask, sections); - in_view.insert(pos, mask); - } - masks.insert(pos, mask); - } - self.in_view_masks = in_view; - self.mesh_gate = gate; - self.vis_valid = true; - - // With occlusion off, push full masks (frustum still applies on the GPU). - if !self.chunk_occlusion_enabled { - for m in masks.values_mut() { - *m = full; - } - } - renderer.set_chunk_visibility(masks); - } - /// Publishes every loaded column's dirty sections to the mesh workers, /// re-meshing a whole column when the player's movement changes its LOD. /// Like vanilla, every section in render distance meshes regardless of @@ -1633,37 +1622,6 @@ impl GameState { } } -/// Extra FOV (radians) for the tier-1 "about to be seen" margin frustum, so -/// small camera turns reveal already-meshed terrain instead of a meshing -/// curtain. -const VIS_MARGIN_RADIANS: f32 = 0.6; - -/// Frustum tier for a column: 0 in view, 1 in the dilated margin, 2 behind the -/// camera. (Nearby columns are forced to 0 by the caller.) -fn column_frustum_tier( - pos: ChunkPos, - eye: glam::DVec3, - planes: &[[f32; 4]; 6], - planes_wide: &[[f32; 4]; 6], - min_y: f32, - max_y: f32, -) -> u8 { - // Camera-relative full-height column box, matching how the GPU cull - // subtracts the eye before its plane test (cull.comp); f64 first for - // precision at extreme coordinates. - let dx = (pos.x as f64 * 16.0 - eye.x) as f32; - let dz = (pos.z as f64 * 16.0 - eye.z) as f32; - let mn = [dx, (min_y as f64 - eye.y) as f32, dz]; - let mx = [dx + 16.0, (max_y as f64 - eye.y) as f32, dz + 16.0]; - if aabb_in_frustum(&mn, &mx, planes) { - 0 - } else if aabb_in_frustum(&mn, &mx, planes_wide) { - 1 - } else { - 2 - } -} - /// The bits for several section indices. pub(crate) fn section_bits(indices: impl IntoIterator) -> u32 { indices @@ -1671,26 +1629,6 @@ pub(crate) fn section_bits(indices: impl IntoIterator) -> u32 { .fold(0u32, |mask, si| mask | section_bit(si)) } -/// Full mask for an `n`-section column (bits `0..n` set). -fn section_mask(n: i32) -> u32 { - if n >= 32 { u32::MAX } else { (1u32 << n) - 1 } -} - -/// Conservative AABB-vs-frustum test (the dominant-corner max-dot used by -/// `cull.comp`): true unless the box is fully behind some plane. -fn aabb_in_frustum(mn: &[f32; 3], mx: &[f32; 3], planes: &[[f32; 4]; 6]) -> bool { - for p in planes { - let d = p[0] * if p[0] >= 0.0 { mx[0] } else { mn[0] } - + p[1] * if p[1] >= 0.0 { mx[1] } else { mn[1] } - + p[2] * if p[2] >= 0.0 { mx[2] } else { mn[2] } - + p[3]; - if d < 0.0 { - return false; - } - } - true -} - pub enum GameUpdateResult { None, ManualDisconnect, @@ -2474,107 +2412,40 @@ fn tick_tool_highlight(core: &AppCore, game: &mut GameState) { game.last_tool_highlight = selected; } -pub fn update_game( +/// Vanilla `DeltaTracker.Timer.advanceGameTime` and `runTick`'s +/// `min(10, ticksToDo)`: every whole tick owed leaves the accumulator and at +/// most ten run. +pub(super) fn ticks_to_run(accumulator: &mut f32) -> u32 { + let mut owed = 0; + while *accumulator >= TICK_RATE { + *accumulator -= TICK_RATE; + owed += 1; + } + owed.min(10) +} + +/// Runs a frame's client ticks, between vanilla `Minecraft.runTick`'s packet +/// processing and its `level.update()`. +fn run_ticks( core: &mut AppCore, - dt: f32, - raw_dt: f32, gfx: &mut Gfx, 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(); - core.audio.set_listener( - listener_pos, - game.player.look_dir.y_rot_deg(), - game.player.look_dir.x_rot_deg(), - ); - core.audio - .update_entity_sound_position(game.player.entity_id, game.player.position); - core.audio.set_volumes(core.menu.category_volumes()); - core.audio.set_subtitles_enabled(core.menu.show_subtitles); - - gfx.renderer.set_vsync(core.menu.vsync); - gfx.renderer - .set_menu_blur_radius(core.menu.menu_background_blurriness); - game.chat.set_options(core.menu.chat_options); - - // Vanilla pauseIfInactive: losing OS focus for more than half a second - // with no screen open pauses the game, which also releases the cursor - // (otherwise a system overlay like Win-key search opens over a still - // captured cursor). TODO: F3+P toggle (options.pauseOnLostFocus). - if core.pause_on_lost_focus - && core - .unfocused_since - .is_some_and(|t| t.elapsed().as_millis() > 500) - && game.input_live() - && !game.dead - { - game.paused = true; - game.pause_screen = PauseScreen::Main; - core.apply_cursor_grab(&gfx.window, Some(game)); - } - - #[cfg(feature = "test-harness")] - if let Some(code) = core.input.take_harness_release() { - game.on_key_released(code, &core.input, connection); - } - - let disconnect_reason = - core.drain_network_events(connection, None, &mut gfx.renderer, &gfx.window, game); - if let Some(reason) = disconnect_reason { - return GameUpdateResult::Disconnected { reason }; - } - - game.chat.tick(); - for mark in game.chat.take_chat_marks() { - connection.packet_tx.mark_chat(mark); - } - - game.drain_and_upload_meshes(&mut gfx.renderer); - - // Sky time ticks unconditionally so it keeps flowing in menus. - core.time_tick_accumulator = (core.time_tick_accumulator + dt).min(1.0); - while core.time_tick_accumulator >= TICK_RATE { - game.sky_state.game_time = game.sky_state.game_time.wrapping_add(1); - game.clocks.tick(game.sky_state.game_time); - game.sync_day_time(); - game.sample_biomes(gfx.renderer.camera_render_position()); - game.environment_probe.tick(game.resolve_environment()); - core.time_tick_accumulator -= TICK_RATE; - } - - if game.input_live() && game.chunk_load_bench.is_none() { - gfx.renderer.update_camera( - &mut core.input, - dt, - core.menu.sensitivity, - core.menu.invert_mouse_x, - core.menu.invert_mouse_y, - ); - } - - // Menus never pause the simulation; tick_physics substitutes neutral input. - core.tick_accumulator += dt; + ticks: u32, +) { // Vanilla `Minecraft.runTick` ticks textures once per frame that runs any // ticks, before them. // TODO: vanilla also skips this while singleplayer is paused and under // `/tick freeze` (`isLevelRunningNormally`); pomme tracks neither yet. - let ticked = core.tick_accumulator >= TICK_RATE; - if ticked { - if game.deterministic { - gfx.renderer.hold_texture_animations(); - } else { - gfx.renderer.tick_texture_animations(); - } + if ticks == 0 { + return; } - while core.tick_accumulator >= TICK_RATE { + if game.deterministic { + gfx.renderer.hold_texture_animations(); + } else { + gfx.renderer.tick_texture_animations(); + } + for _ in 0..ticks { game.tick_count = game.tick_count.wrapping_add(1); if !game.deterministic { game.block_light_flicker = tick_block_light_flicker(game.block_light_flicker); @@ -2663,16 +2534,111 @@ pub fn update_game( game.xp_display_start_tick = game.tick_count as i64; } AppCore::send_client_tick_end(connection); - core.tick_accumulator -= TICK_RATE; } - if ticked { - gfx.renderer.tick_lightmap(lightmap_info( - game, - core.menu.brightness, - sky_partial_tick(core), - )); + gfx.renderer.tick_lightmap(lightmap_info( + game, + core.menu.brightness, + sky_partial_tick(core), + )); +} + +pub fn update_game( + core: &mut AppCore, + dt: f32, + raw_dt: f32, + gfx: &mut Gfx, + 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(); + core.audio.set_listener( + listener_pos, + game.player.look_dir.y_rot_deg(), + game.player.look_dir.x_rot_deg(), + ); + core.audio + .update_entity_sound_position(game.player.entity_id, game.player.position); + core.audio.set_volumes(core.menu.category_volumes()); + core.audio.set_subtitles_enabled(core.menu.show_subtitles); + + gfx.renderer.set_vsync(core.menu.vsync); + gfx.renderer + .set_menu_blur_radius(core.menu.menu_background_blurriness); + game.chat.set_options(core.menu.chat_options); + + // Vanilla pauseIfInactive: losing OS focus for more than half a second + // with no screen open pauses the game, which also releases the cursor + // (otherwise a system overlay like Win-key search opens over a still + // captured cursor). TODO: F3+P toggle (options.pauseOnLostFocus). + if core.pause_on_lost_focus + && core + .unfocused_since + .is_some_and(|t| t.elapsed().as_millis() > 500) + && game.input_live() + && !game.dead + { + game.paused = true; + game.pause_screen = PauseScreen::Main; + core.apply_cursor_grab(&gfx.window, Some(game)); + } + + #[cfg(feature = "test-harness")] + if let Some(code) = core.input.take_harness_release() { + game.on_key_released(code, &core.input, connection); + } + + // The ticks the loading screen handed over finish before any packet is + // handled, as vanilla's join tick does. + let carried = std::mem::take(&mut core.carried_ticks); + run_ticks(core, gfx, connection, game, carried); + + // Added before the drain, which takes every packet when a tick is owed. + core.tick_accumulator += dt; + let disconnect_reason = + core.drain_network_events(connection, None, &mut gfx.renderer, &gfx.window, game); + if let Some(reason) = disconnect_reason { + return GameUpdateResult::Disconnected { reason }; + } + + game.chat.tick(); + for mark in game.chat.take_chat_marks() { + connection.packet_tx.mark_chat(mark); + } + + game.drain_and_upload_meshes(&mut gfx.renderer); + + // Sky time ticks unconditionally so it keeps flowing in menus. + core.time_tick_accumulator = (core.time_tick_accumulator + dt).min(1.0); + while core.time_tick_accumulator >= TICK_RATE { + game.sky_state.game_time = game.sky_state.game_time.wrapping_add(1); + game.clocks.tick(game.sky_state.game_time); + game.sync_day_time(); + game.sample_biomes(gfx.renderer.camera_render_position()); + game.environment_probe.tick(game.resolve_environment()); + core.time_tick_accumulator -= TICK_RATE; + } + + if game.input_live() && game.chunk_load_bench.is_none() { + gfx.renderer.update_camera( + &mut core.input, + dt, + core.menu.sensitivity, + core.menu.invert_mouse_x, + core.menu.invert_mouse_y, + ); } + // Menus never pause the simulation; tick_physics substitutes neutral input. + let ticks = ticks_to_run(&mut core.tick_accumulator); + run_ticks(core, gfx, connection, game, ticks); + // Once per frame after the frame's ticks, where vanilla `Minecraft.runTick` // calls `level.update()`. game.update_light(); @@ -2915,6 +2881,7 @@ pub fn update_game( }), chunk_count: gfx.renderer.loaded_chunk_count(), sections_drawn: gfx.renderer.sections_drawn(), + triangles_drawn: gfx.renderer.last_timings().triangles, occlusion_on: game.chunk_occlusion_enabled, // The middle slot is unused. mesh_gate: game @@ -2925,6 +2892,7 @@ pub fn update_game( screen_w: gfx.renderer.screen_width(), screen_h: gfx.renderer.screen_height(), timings: Some(hud::FrameTimings { + gpu: gfx.renderer.last_timings().gpu.clone(), pace_ms: gfx.renderer.last_timings().pace_ms, frame_ms: gfx.renderer.last_timings().frame_ms, fence_ms: gfx.renderer.last_timings().fence_ms, @@ -4551,6 +4519,30 @@ pub fn update_game( } else { core.menu.render_distance }; + // Off without sky light (see `renderer::distant`'s TODO); capturing + // stops with it. + let distant_terrain = if game.light_engine.has_sky() { + core.menu.distant_terrain + } else { + 0 + }; + if game.distant.set_view( + &game.dimension, + game.player.position.x, + game.player.position.z, + distant_terrain, + ) { + for pos in game.chunk_store.loaded_positions() { + game.keep_column(pos); + } + } + gfx.renderer + .set_distant_view((distant_terrain > 0).then(|| DistantView { + dimension: game.dimension.as_str().into(), + min_y: game.chunk_store.min_y(), + height: game.chunk_store.height() as i32, + radius: f64::from(distant_terrain * 16), + })); let inventory = &game.player.inventory; let main_stack = &inventory.hotbar_slots()[core.input.selected_slot() as usize]; let shown_hands = game @@ -4653,6 +4645,7 @@ pub fn update_game( core.menu.cloud_mode }, effective_rd, + distant_terrain, player_preview, book_preview, game.player.eyes_in_water, @@ -6073,8 +6066,23 @@ fn lightmap_info(game: &GameState, brightness: f32, partial_tick: f32) -> Lightm mod tests { use super::{ MeshGate, detached_distance, has_red_overlay, scroll_wheel, section_bit, section_bits, - tick_block_light_flicker, + tick_block_light_flicker, ticks_to_run, }; + use crate::app::TICK_RATE; + + #[test] + fn owed_ticks_run_and_keep_the_residual() { + let mut acc = 2.5 * TICK_RATE; + assert_eq!(ticks_to_run(&mut acc), 2); + assert!((acc - 0.5 * TICK_RATE).abs() < 1e-4); + } + + #[test] + fn owed_ticks_cap_at_ten_and_drop_the_rest() { + let mut acc = 25.5 * TICK_RATE; + assert_eq!(ticks_to_run(&mut acc), 10); + assert!(acc < TICK_RATE); + } #[test] fn mesh_gate_counts_masks_and_takes_them_back() { diff --git a/pomme-client/src/cpu_bench/distant.rs b/pomme-client/src/cpu_bench/distant.rs new file mode 100644 index 000000000..55a9922f8 --- /dev/null +++ b/pomme-client/src/cpu_bench/distant.rs @@ -0,0 +1,107 @@ +//! Distant terrain's worker: a forgotten chunk voxelized and mipped into +//! every level, and a section meshed, per scene; and a backlog of columns +//! larger than the section cache taken in again from disk, in queue order +//! and in the order the thread takes its last backlog in. + +use std::hint::black_box; + +use criterion::Criterion; + +use super::fixtures::{Assets, Scene, build_world, columns_within}; +use crate::position::ChunkPos; +use crate::renderer::distant::build::Factory; +use crate::renderer::distant::models::ModelFactory; +use crate::renderer::distant::section::{LEVELS, SectionKey}; +use crate::renderer::distant::voxel::States; +use crate::renderer::distant::world::{World, ingest_order}; + +pub fn bench(c: &mut Criterion, assets: &Assets) { + let colors = assets.mesh_dispatcher().distant_colors(); + let states = States::new(assets.block_state_table); + + let mut group = c.benchmark_group("distant"); + for scene in Scene::ALL { + let world = build_world(scene, assets.block_state_table); + let ingest = |into: &mut World, pos: ChunkPos| { + let chunk = world + .get_chunk(&pos) + .expect("the scene's columns are loaded"); + let light = world.light_data.get(&(pos.x, pos.z)).cloned(); + into.ingest_chunk( + &states, + pos, + &chunk.read().sections, + light.as_deref(), + world.min_y(), + ) + }; + group.bench_function(format!("ingest/{}", scene.name()), |b| { + b.iter(|| black_box(ingest(&mut World::default(), ChunkPos::new(0, 0)))) + }); + + let mut distant = World::default(); + for pos in columns_within(1) { + ingest(&mut distant, pos); + } + // The section holding the surface at full detail, and the one + // holding everything at the coarsest. + for key in [ + SectionKey::of_vanilla(0, 0, 4, 0), + SectionKey::of_vanilla(LEVELS - 1, 0, 0, 0), + ] { + let section = distant.get(key).expect("chunk (0, 0)'s sections"); + let mut models = ModelFactory::new(); + let mut factory = Factory::new(); + let build = |factory: &mut Factory, models: &mut ModelFactory| { + factory + .build(key, section, |k| distant.get(k), &mut colors.baking(models)) + .quads + .len() + }; + // Models bake once, before the meshing measured. + build(&mut factory, &mut models); + group.bench_function(format!("build/{}/level{}", scene.name(), key.level), |b| { + b.iter(|| black_box(build(&mut factory, &mut models))) + }); + } + } + group.finish(); + backlog(c, assets, &states); +} + +/// Columns per side of the backlog: 48² columns hold about 3600 level-0 +/// sections, past the cache. +const BACKLOG_SIDE: i32 = 48; + +fn backlog(c: &mut Criterion, assets: &Assets, states: &States) { + let world = build_world(Scene::Plains, assets.block_state_table); + let chunk = world + .get_chunk(&ChunkPos::new(0, 0)) + .expect("the scene's columns are loaded"); + let sections = chunk.read().sections.clone(); + let light = world.light_data.get(&(0, 0)).cloned(); + let dir = std::env::temp_dir().join(format!("pomme-bench-distant-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + let ingest = |columns: &[ChunkPos]| { + let mut into = World::new(Some(dir.clone())); + for &pos in columns { + into.ingest_chunk(states, pos, §ions, light.as_deref(), world.min_y()); + } + into.flush(); + }; + let mut columns: Vec = (0..BACKLOG_SIDE) + .flat_map(|x| (0..BACKLOG_SIDE).map(move |z| ChunkPos::new(x, z))) + .collect(); + fastrand::Rng::with_seed(7).shuffle(&mut columns); + // Everything on disk already, as on the way out. + ingest(&columns); + + let mut group = c.benchmark_group("distant/backlog"); + group.sample_size(10); + group.bench_function("queued", |b| b.iter(|| ingest(black_box(&columns)))); + let mut ordered = columns.clone(); + ordered.sort_by_key(|&pos| ingest_order(pos)); + group.bench_function("z-order", |b| b.iter(|| ingest(black_box(&ordered)))); + group.finish(); + let _ = std::fs::remove_dir_all(dir); +} diff --git a/pomme-client/src/cpu_bench/mod.rs b/pomme-client/src/cpu_bench/mod.rs index 2348b00ee..685a20388 100644 --- a/pomme-client/src/cpu_bench/mod.rs +++ b/pomme-client/src/cpu_bench/mod.rs @@ -1,10 +1,11 @@ //! Headless CPU benchmarks (`just bench [filter]`): the chunk mesher and its -//! worker queue, the chunk draw lists, cave culling and chunk packet -//! decoding, with no window or Vulkan device. +//! worker queue, the chunk draw lists, cave culling, chunk packet decoding +//! and distant terrain, with no window or Vulkan device. //! Criterion reports each run's change against the last. mod chunk_decode; mod dispatch; +mod distant; mod fixtures; mod lists; mod mesh; @@ -28,7 +29,7 @@ pub fn run(version: &str, dirs: &DataDirs, filter: Option<&str>) -> i32 { // The mesh and visgraph fixtures load block render assets and the atlas from // disk; skip that when the filter leaves neither group to run. let needs_assets = filter.is_none_or(|f| { - ["mesh", "dispatch", "visgraph"] + ["mesh", "dispatch", "visgraph", "distant"] .iter() .any(|group| group.contains(f) || f.contains(group)) }); @@ -39,6 +40,7 @@ pub fn run(version: &str, dirs: &DataDirs, filter: Option<&str>) -> i32 { mesh::bench(&mut c, &assets); dispatch::bench(&mut c, &assets); occlusion::bench_sections(&mut c, &assets); + distant::bench(&mut c, &assets); } chunk_decode::bench(&mut c, block_state_table); lists::bench(&mut c); diff --git a/pomme-client/src/harness/exec.rs b/pomme-client/src/harness/exec.rs index ee45f32f4..f371d5787 100644 --- a/pomme-client/src/harness/exec.rs +++ b/pomme-client/src/harness/exec.rs @@ -235,6 +235,9 @@ fn set_options(core: &mut AppCore, options: &protocol::OptionSet) -> Result, /// Render Distance in chunks, clamped to the slider's range. pub render_distance: Option, + /// Distant Terrain in chunks, 0 for off, else clamped to 32..256. + pub distant_terrain: Option, } #[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)] diff --git a/pomme-client/src/harness/script.rs b/pomme-client/src/harness/script.rs index ddec61518..764f49a5d 100644 --- a/pomme-client/src/harness/script.rs +++ b/pomme-client/src/harness/script.rs @@ -87,6 +87,7 @@ enum Expect { Block(BlockExpect), Biome(BiomeExpect), Terrain(TerrainExpect), + Distant(DistantExpect), Menu(MenuExpect), Advancement(AdvancementExpect), Chat(ChatExpect), @@ -241,6 +242,23 @@ struct TerrainExpect { terrain: String, } +/// Distant terrain is drawn at a block column, `{ at = [x, z] }`: the +/// finest section holding it is meshed and uploaded; with `gone = true`, +/// it isn't. +#[derive(Debug, PartialEq, Deserialize)] +#[serde(deny_unknown_fields)] +struct DistantExpect { + distant_terrain: DistantAt, +} + +#[derive(Debug, PartialEq, Deserialize)] +#[serde(deny_unknown_fields)] +struct DistantAt { + at: [i32; 2], + #[serde(default)] + gone: bool, +} + /// The open menu: a container's menu type (`generic_9x3`), `inventory`, /// `creative`, `pause` or `none`, and optionally its title. #[derive(Debug, PartialEq, Deserialize)] @@ -387,10 +405,14 @@ struct FrameSample { interval_ms: f32, frame_ms: f32, fence_ms: f32, + gpu: Vec<(&'static str, f32)>, + sections: u32, + triangles: u32, } /// `samples` summed up: the frame rate, its 1% low, the time the renderer -/// takes to encode a world frame, and the wait for the GPU. +/// takes to encode a world frame, the wait for the GPU, the GPU's own time +/// per pass, and what the chunk draws covered. fn summarize(samples: &[FrameSample]) -> String { let mean = |f: fn(&FrameSample) -> f32| samples.iter().map(f).sum::() / samples.len() as f32; @@ -398,13 +420,44 @@ fn summarize(samples: &[FrameSample]) -> String { intervals.sort_by(f32::total_cmp); let slowest = &intervals[intervals.len() - intervals.len().div_ceil(100)..]; let low = slowest.iter().sum::() / slowest.len() as f32; + // Per pass, in the order the frame stamps them. + let mut passes: Vec<(&str, f32, u32)> = Vec::new(); + for &(name, ms) in samples.iter().flat_map(|s| &s.gpu) { + match passes.iter_mut().find(|pass| pass.0 == name) { + Some(pass) => { + pass.1 += ms; + pass.2 += 1; + } + None => passes.push((name, ms, 1)), + } + } + let passes: Vec<(&str, f32)> = passes + .into_iter() + .map(|(name, sum, count)| (name, sum / count as f32)) + .collect(); + // A device without timestamps stamps nothing. + let gpu = if passes.is_empty() { + String::new() + } else { + format!( + "; gpu {:.2} ms: {}", + passes.iter().map(|(_, ms)| ms).sum::(), + passes + .iter() + .map(|(name, ms)| format!("{name} {ms:.2}")) + .collect::>() + .join(", "), + ) + }; format!( - "{} frames: {:.1} fps, 1% low {:.1} fps; encode {:.2} ms, gpu wait {:.2} ms", + "{} frames: {:.1} fps, 1% low {:.1} fps; encode {:.2} ms, gpu wait {:.2} ms{gpu}; {:.0} sections, {:.0}k triangles", samples.len(), 1000.0 / mean(|s| s.interval_ms), 1000.0 / low, mean(|s| s.frame_ms), mean(|s| s.fence_ms), + mean(|s| s.sections as f32), + mean(|s| s.triangles as f32) / 1000.0, ) } @@ -483,6 +536,9 @@ impl Script { interval_ms: (now - last).as_secs_f32() * 1000.0, frame_ms: timings.frame_ms, fence_ms: timings.fence_ms, + gpu: timings.gpu.clone(), + sections: timings.sections, + triangles: timings.triangles, }); } self.last_frame = Some(now); @@ -791,6 +847,12 @@ fn check(expect: &Expect, phase: &AppPhase) -> Result<(), String> { || "the chunk arenas are still compacting".into(), ) } + Expect::Distant(DistantExpect { + distant_terrain: DistantAt { at: [x, z], gone }, + }) => ensure(gfx.renderer.has_distant_column(*x, *z) != *gone, || { + let state = if *gone { "still" } else { "no" }; + format!("{state} distant terrain at {x}, {z}") + }), Expect::Biome(BiomeExpect { biome, at }) => { let seen = protocol::biome(game, *at); ensure(seen == *biome, || format!("{at:?} is in {seen}")) @@ -920,6 +982,7 @@ mod tests { brightness: None, shader_pack: None, render_distance: None, + distant_terrain: None, })) ); assert_eq!( @@ -1006,11 +1069,18 @@ mod tests { interval_ms: if i < 2 { 50.0 } else { 10.0 }, frame_ms: 1.0, fence_ms: 0.5, + gpu: if i % 2 == 0 { + vec![("chunks", 0.25), ("distant", 0.5)] + } else { + vec![("chunks", 0.75)] + }, + sections: 10, + triangles: 2000, }) .collect(); assert_eq!( summarize(&samples), - "200 frames: 96.2 fps, 1% low 20.0 fps; encode 1.00 ms, gpu wait 0.50 ms" + "200 frames: 96.2 fps, 1% low 20.0 fps; encode 1.00 ms, gpu wait 0.50 ms; gpu 1.00 ms: chunks 0.50, distant 0.50; 10 sections, 2k triangles" ); } } diff --git a/pomme-client/src/headless.rs b/pomme-client/src/headless.rs index 9ac52958b..e03275b22 100644 --- a/pomme-client/src/headless.rs +++ b/pomme-client/src/headless.rs @@ -93,6 +93,7 @@ pub fn run( skin_options, Rc::new(BlockDefinitions::new()), crate::version::selected_protocol(), + None, ); let code = drive( @@ -243,8 +244,9 @@ fn drive( game.rescan_mesh_jobs(chunk_detail); game.last_update_phases.rescan_ms = ms(t_rescan); - let batch = game.drain_meshes(); - for mesh in &batch { + let mut batch = game.drain_meshes(); + for mesh in &mut batch { + game.apply_mesh_bookkeeping(mesh); let mask = drawn.entry(mesh.pos).or_default(); *mask &= !section_bits(mesh.replaced.clone()); *mask |= section_bits(mesh.sections.iter().map(|s| s.section_index)); diff --git a/pomme-client/src/lib.rs b/pomme-client/src/lib.rs index eeeddf2b1..03b5e4e4b 100644 --- a/pomme-client/src/lib.rs +++ b/pomme-client/src/lib.rs @@ -53,7 +53,6 @@ pub use winit::platform::android::activity::AndroidApp; use crate::app::App; pub use crate::args::{LAUNCH_FILE, LaunchArgs}; -#[cfg(target_os = "android")] pub use crate::logging::install_panic_hook; use crate::user::UserData; @@ -273,11 +272,14 @@ pub fn run(args: LaunchArgs, platform: Platform) { tracing::error!("Fatal: {e}"); std::process::exit(1); } + let code = app.chunk_bench_exit_code(); #[cfg(feature = "test-harness")] - if let Some(code) = app.harness_exit_code() { - std::process::exit(code); - } - if let Some(code) = app.chunk_bench_exit_code() { + let code = app.harness_exit_code().or(code); + // Dropped first, so the distant terrain thread is told to write what it + // kept; then waited for, the window already gone. + drop(app); + crate::renderer::distant::worker::wait_for_stopped(std::time::Duration::from_secs(30)); + if let Some(code) = code { std::process::exit(code); } } diff --git a/pomme-client/src/logging.rs b/pomme-client/src/logging.rs index 7a12db716..9cee3bd79 100644 --- a/pomme-client/src/logging.rs +++ b/pomme-client/src/logging.rs @@ -1,4 +1,5 @@ -use std::path::Path; +use std::path::{Path, PathBuf}; +use std::sync::OnceLock; use tracing_appender::non_blocking::WorkerGuard; use tracing_subscriber::prelude::*; @@ -14,7 +15,8 @@ pub fn init(log_dir: &Path) -> Option { if let Err(e) = rotate(log_dir) { eprintln!("Failed to rotate logs: {e}. latest.log will probably be overwritten."); } - let file_appender = tracing_appender::rolling::never(log_dir, "latest.log"); + let _ = LOG_FILE.set(log_dir.join(LATEST_LOG)); + let file_appender = tracing_appender::rolling::never(log_dir, LATEST_LOG); // The default queues 128k lines up front (~4 MB); a file write keeps up // with far fewer, and a full queue drops lines rather than blocking. let (non_blocking, guard) = tracing_appender::non_blocking::NonBlockingBuilder::default() @@ -42,20 +44,45 @@ pub fn init(log_dir: &Path) -> Option { Some(guard) } -/// Panics to the log. Android drops stderr, so one before logging starts goes -/// straight to logcat. -#[cfg(target_os = "android")] +const LATEST_LOG: &str = "latest.log"; +/// The log file, for the panic hook. +static LOG_FILE: OnceLock = OnceLock::new(); + +/// Panics to the log, written on the spot: the abort that follows one +/// (`app::state_slot`) never lets the log's writer thread get to it, and a +/// release build has no console for the default hook's message. Android +/// drops stderr, so one before logging starts goes straight to logcat. pub fn install_panic_hook() { - std::panic::set_hook(Box::new(|info| { - if tracing::dispatcher::has_been_set() { - tracing::error!("{info}"); - } else { - let logcat = tracing_subscriber::registry().with(console_layer()); - tracing::subscriber::with_default(logcat, || tracing::error!("{info}")); - } + let default_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + log_panic(info); + default_hook(info); })); } +fn log_panic(info: &std::panic::PanicHookInfo) { + let path = LOG_FILE.get(); + if let Some(path) = path { + let line = format!("{} PANIC {info}\n", chrono::Utc::now().to_rfc3339()); + let _ = std::fs::OpenOptions::new() + .append(true) + .create(true) + .open(path) + .and_then(|mut file| std::io::Write::write_all(&mut file, line.as_bytes())); + } + // The default hook prints it to stderr, which Android drops, so logcat + // gets it alone: the global dispatch would write the file a second time. + #[cfg(target_os = "android")] + { + let logcat = tracing_subscriber::registry().with(console_layer()); + tracing::subscriber::with_default(logcat, || tracing::error!("{info}")); + } + #[cfg(not(target_os = "android"))] + if path.is_none() && tracing::dispatcher::has_been_set() { + tracing::error!("{info}"); + } +} + /// Stdout, or logcat on Android, which drops stdout. fn console_layer() -> impl tracing_subscriber::Layer where @@ -70,7 +97,7 @@ where const MAX_LOG_FILES: usize = 5; fn rotate(log_dir: &Path) -> std::io::Result<()> { - let latest = log_dir.join("latest.log"); + let latest = log_dir.join(LATEST_LOG); if !latest.exists() { return Ok(()); } @@ -121,3 +148,40 @@ fn cleanup_old_logs(log_dir: &Path) { let _ = std::fs::remove_file(entry.path()); } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn panic_lands_in_the_log() { + let nanos = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos(); + let dir = std::env::temp_dir().join(format!( + "pomme-panic-hook-test-{}-{nanos}", + std::process::id() + )); + std::fs::create_dir_all(&dir).unwrap(); + let log = dir.join(LATEST_LOG); + LOG_FILE.set(log.clone()).unwrap(); + + // Process-wide while it runs; the previous hook comes back after. + let previous = std::sync::Arc::new(std::panic::take_hook()); + let chained = std::sync::Arc::clone(&previous); + std::panic::set_hook(Box::new(move |info| { + log_panic(info); + chained(info); + })); + let _ = std::panic::catch_unwind(|| panic!("the message under test")); + drop(std::panic::take_hook()); + if let Ok(previous) = std::sync::Arc::try_unwrap(previous) { + std::panic::set_hook(previous); + } + + let written = std::fs::read_to_string(&log).unwrap(); + let _ = std::fs::remove_dir_all(&dir); + assert!(written.contains("PANIC") && written.contains("the message under test")); + } +} diff --git a/pomme-client/src/main.rs b/pomme-client/src/main.rs index 7900e25c3..53333e9e9 100644 --- a/pomme-client/src/main.rs +++ b/pomme-client/src/main.rs @@ -5,6 +5,7 @@ use clap::Parser; fn main() { + pomme_client::install_panic_hook(); pomme_client::run( pomme_client::LaunchArgs::parse(), pomme_client::Platform { data_root: None }, diff --git a/pomme-client/src/renderer/camera.rs b/pomme-client/src/renderer/camera.rs index 8741ebfcf..f872f0a20 100644 --- a/pomme-client/src/renderer/camera.rs +++ b/pomme-client/src/renderer/camera.rs @@ -13,6 +13,10 @@ pub const MIN_FOV_DEGREES: f32 = 30.0; #[allow(dead_code)] pub const MAX_FOV_DEGREES: f32 = 110.0; const NEAR: f32 = 0.1; +/// Near plane of distant terrain's projection. Its nearest column is past +/// the loaded chunks, so a near plane this far out keeps depth precise +/// across thousands of blocks. +const DISTANT_NEAR: f32 = 16.0; /// Floor of the dynamic far plane (vanilla floors at `cloudRange * 16` /// instead; pomme's fixed-reach clouds derive their fade from this). pub(crate) const MIN_FAR: f32 = 1000.0; @@ -577,10 +581,30 @@ impl Camera { } pub fn view_projection_with_fov(&self, fov: f32) -> Mat4 { + self.view_projection_with_planes(fov, NEAR, self.depth_far) + } + + /// The view projection distant terrain draws with, out to `far` blocks. + pub fn distant_view_projection(&self, far: f32) -> Mat4 { + self.view_projection_with_planes( + self.fov_radians(self.render_partial_tick), + DISTANT_NEAR, + far, + ) + } + + /// [`Self::distant_view_projection`] with the field of view widened by + /// `extra_radians` (clamped below 180°): what is about to come into view. + pub fn distant_view_projection_dilated(&self, far: f32, extra_radians: f32) -> Mat4 { + let fov = (self.fov_radians(self.render_partial_tick) + extra_radians).min(2.96); + self.view_projection_with_planes(fov, DISTANT_NEAR, far) + } + + fn view_projection_with_planes(&self, fov: f32, near: f32, far: f32) -> Mat4 { let offset = self.third_person_offset(); let (forward, up) = self.view_basis(); let view = self.view_effect_matrix() * view::look_to_mat4(offset, forward, up); - let mut proj = proj::directx::perspective(fov, self.aspect_ratio, NEAR, self.depth_far); + let mut proj = proj::directx::perspective(fov, self.aspect_ratio, near, far); proj.y_axis.y *= -1.0; // Vulkan NDC has +Y down proj * view } @@ -616,13 +640,15 @@ const WATER_FOG_END: f32 = 96.0; impl CameraUniform { /// The camera with its fog: `sky`'s fog colour, and the environmental band - /// from its fog distances. + /// from its fog distances, scaled by `haze_scale` out of water (distant + /// terrain stretches the haze over its distance). /// TODO: the rain offsets `AtmosphericFogEnvironment` applies to them. pub fn new( camera: &Camera, sky: &SkyState, render_distance_chunks: u32, eyes_in_water: bool, + haze_scale: f32, ) -> Self { let fog_color = sky.fog_color.to_array(); let anchor = camera.anchor(); @@ -654,8 +680,8 @@ impl CameraUniform { ( blocks - span, blocks, - attributes.fog_start_distance, - attributes.fog_end_distance, + attributes.fog_start_distance * haze_scale, + attributes.fog_end_distance * haze_scale, fog_color, ) }; diff --git a/pomme-client/src/renderer/chunk/atlas.rs b/pomme-client/src/renderer/chunk/atlas.rs index 2190ce71e..b940af206 100644 --- a/pomme-client/src/renderer/chunk/atlas.rs +++ b/pomme-client/src/renderer/chunk/atlas.rs @@ -1,5 +1,6 @@ use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; +use std::sync::Arc; use crate::assets::{AssetId, AssetIndex}; use crate::renderer::{packing, rhi, util}; @@ -47,6 +48,17 @@ impl AtlasRegion { }; } +/// A block sprite's first frame, level 0. +#[derive(Clone)] +pub(crate) struct SpritePixels { + pub width: u32, + pub height: u32, + /// RGBA, row by row from the top. + pub rgba: Arc<[u8]>, + /// Mipped as `dark_cutout`, which leaves transparent texels black. + pub dark_cutout: bool, +} + #[derive(Clone)] pub struct AtlasUVMap { regions: HashMap, @@ -54,6 +66,9 @@ pub struct AtlasUVMap { /// built; later ones fall back to the name lookup. by_texture: Vec, sprite_alpha_masks: HashMap, + /// Each block sprite's pixels, which distant terrain bakes its models + /// from. + sprite_pixels: HashMap, /// Level-0 rectangles by sprite index, as `(x, y, width, height)`. rects: Vec<[u32; 4]>, missing: AtlasRegion, @@ -63,6 +78,7 @@ impl AtlasUVMap { fn new( regions: HashMap, sprite_alpha_masks: HashMap, + sprite_pixels: HashMap, rects: Vec<[u32; 4]>, missing: AtlasRegion, ) -> Self { @@ -74,6 +90,7 @@ impl AtlasUVMap { regions, by_texture, sprite_alpha_masks, + sprite_pixels, rects, missing, } @@ -102,6 +119,10 @@ impl AtlasUVMap { self.sprite_alpha_masks.get(name) } + pub(crate) fn sprite_pixels(&self, name: &str) -> Option<&SpritePixels> { + self.sprite_pixels.get(name) + } + /// Each sprite's texture name, by sprite index. #[cfg(feature = "cpu-bench")] pub fn sprite_names(&self) -> HashMap { @@ -966,6 +987,7 @@ impl TextureAtlas { let mut regions = HashMap::new(); let mut sprite_alpha_masks = HashMap::new(); + let mut sprite_pixels = HashMap::new(); let mut rects = vec![missing_region.pixel_rect.map(u32::from)]; for src in &sources { match placements.get(src.name.as_str()) { @@ -992,6 +1014,18 @@ impl TextureAtlas { if let Some(mask) = &src.alpha_mask { sprite_alpha_masks.insert(src.name.clone(), mask.clone()); } + if let Some(mip) = &src.mip_source { + let texels = (src.w * src.h * 4) as usize; + sprite_pixels.insert( + src.name.clone(), + SpritePixels { + width: src.w, + height: src.h, + rgba: src.data[..texels].into(), + dark_cutout: mip.strategy == MipmapStrategy::DarkCutout, + }, + ); + } regions.insert(src.name.clone(), region); } _ => { @@ -1000,7 +1034,13 @@ impl TextureAtlas { } } - let uv_map = AtlasUVMap::new(regions, sprite_alpha_masks, rects, missing_region); + let uv_map = AtlasUVMap::new( + regions, + sprite_alpha_masks, + sprite_pixels, + rects, + missing_region, + ); AtlasCpu { uv_map, @@ -1580,7 +1620,7 @@ mod tests { ("test/region_before_build".to_owned(), named), ("test/region_after_build".to_owned(), late_named), ]); - let uv_map = AtlasUVMap::new(regions, HashMap::new(), Vec::new(), missing); + let uv_map = AtlasUVMap::new(regions, HashMap::new(), HashMap::new(), Vec::new(), missing); // Interned after the map was built: found through the name instead. let late = TextureId::intern("test/region_after_build"); assert_eq!(uv_map.region(built).pixel_rect, named.pixel_rect); diff --git a/pomme-client/src/renderer/chunk/buffer.rs b/pomme-client/src/renderer/chunk/buffer.rs index cd2c9427b..6747f7b3a 100644 --- a/pomme-client/src/renderer/chunk/buffer.rs +++ b/pomme-client/src/renderer/chunk/buffer.rs @@ -1,5 +1,6 @@ use std::collections::{HashMap, HashSet, VecDeque}; use std::mem::offset_of; +use std::sync::Arc; use glam::DVec3; @@ -79,8 +80,8 @@ const CULL_BINDINGS: &[rhi::BindGroupLayoutEntry] = &[ rhi::BindGroupLayoutEntry::storage(rhi::ShaderStages::COMPUTE), rhi::BindGroupLayoutEntry::storage(rhi::ShaderStages::COMPUTE), ]; -/// A draw count per arena. -const COUNTS_SIZE: u64 = (MAX_ARENAS * size_of::()) as u64; +/// A draw count per arena, then an index count per arena. +const COUNTS_SIZE: u64 = (2 * MAX_ARENAS * size_of::()) as u64; const FRUSTUM_SIZE: u64 = size_of::() as u64; /// Column slots the visibility masks start with room for. const INITIAL_COLUMNS: usize = 1 << 14; @@ -90,6 +91,7 @@ const MIN_SEGMENT: u32 = 64; /// Staging buffers in flight: one per frame that may still read one, and one /// written while they do. const STAGING_SLOTS: usize = MAX_FRAMES_IN_FLIGHT + 1; + /// Columns within this squared X/Z distance of the camera render opaque /// immediately and never fade in; `cull.comp` has the same. const NEARBY_DIST_SQ: f32 = 768.0; @@ -311,14 +313,15 @@ const fn chunk_vertex_buffers( ] } +/// `VkDrawIndexedIndirectCommand`. #[repr(C)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct DrawCommand { - index_count: u32, - instance_count: u32, - first_index: u32, - vertex_offset: i32, - first_instance: u32, +pub(crate) struct DrawCommand { + pub index_count: u32, + pub instance_count: u32, + pub first_index: u32, + pub vertex_offset: i32, + pub first_instance: u32, } /// An integer section origin widened for f64 math. @@ -905,6 +908,9 @@ pub struct ChunkUploads { pub remesh: Vec<(ChunkPos, Vec)>, /// Sections that found no room, or were evicted for nearer ones. pub starved: Vec<(ChunkPos, Vec)>, + /// How many meshes were taken, from the front; the rest didn't fit the + /// frame's staging room. + pub taken: usize, } /// What a column's sections take: in total, and the largest one's slices. @@ -1393,7 +1399,7 @@ pub struct ChunkBufferStore { /// Per-column bitmask of occlusion-visible section indices (bit `si`), from /// the CPU visibility graph. A column absent here defaults to fully /// visible, so freshly-loaded-but-not-yet-graphed columns still draw. - chunk_visibility: HashMap, + chunk_visibility: Arc>, /// The masks by column slot, as the cull reads them, and the frame slots /// still to receive them. column_visibility: Vec, @@ -1430,6 +1436,8 @@ pub struct ChunkBufferStore { /// Post-cull section draw count read back from the GPU (lags a few frames); /// exposed for the debug overlay so occlusion's effect is visible. last_draw_count: u32, + /// Indices the last cull's draws of every layer covered. + last_index_count: u32, /// Monotonic frame counter, bumped once per rendered frame in /// `begin_frame`. @@ -1519,7 +1527,7 @@ impl ChunkBufferStore { add_arena_after: 0, add_arena_failures: 0, lists: Lists::default(), - chunk_visibility: HashMap::new(), + chunk_visibility: Arc::default(), column_visibility: Vec::new(), visibility_pending: 0, bare_uploads: HashMap::new(), @@ -1534,7 +1542,7 @@ impl ChunkBufferStore { rhi::backend::Buffer::new( device, "draw_count_readback", - 2 * COUNTS_SIZE, + 3 * COUNTS_SIZE, rhi::BufferUsages::COPY_DST, rhi::MemoryLocation::Readback, ) @@ -1547,6 +1555,7 @@ impl ChunkBufferStore { cull_layout, fade_enabled: false, last_draw_count: 0, + last_index_count: 0, frame_seq: 0, pending_free: VecDeque::new(), } @@ -1558,6 +1567,11 @@ impl ChunkBufferStore { self.last_draw_count } + /// Triangles the last cull drew, every layer. + pub fn triangles_drawn(&self) -> u32 { + self.last_index_count / 3 + } + /// Milliseconds since the store was made, never 0 (no fade). fn now_ms(&self) -> u32 { (self.epoch.elapsed().as_millis() as u32).max(1) @@ -1619,15 +1633,17 @@ impl ChunkBufferStore { /// Upload a batch of mesh results, each replacing the sections in its /// `mesh.replaced` range. The vertices go to this frame's staging buffer /// and the copies into the arenas are recorded as the frame starts, so a - /// streaming frame never waits for the GPU; only a batch outgrowing the - /// staging buffer is flushed and waited for on the spot. + /// streaming frame never waits for the GPU: a batch outgrowing the + /// staging buffer stops there, and the rest is the caller's to send next + /// frame. Also returns the starved sections that may fit now, per column, + /// for re-meshing. pub fn upload_batch( &mut self, device: &rhi::backend::Device, meshes: &[ChunkMeshData], ) -> ChunkUploads { if meshes.is_empty() { - return self.uploads(device); + return self.uploads(device, 0); } // Sub-allocate an exact-size vertex + index slice for each non-empty @@ -1689,7 +1705,8 @@ impl ChunkBufferStore { } let now_ms = self.now_ms(); - for mesh in meshes { + let mut taken = meshes.len(); + for (i, mesh) in meshes.iter().enumerate() { // The covered sections this job is authoritative for: reject any where // a newer upload (higher epoch) already landed. See // `ChunkMeshData::upload_epoch`. @@ -1707,9 +1724,6 @@ impl ChunkBufferStore { mesh.upload_epoch >= stored }) .collect(); - // Placed or starved again below, they no longer hold the room. - let unheld = self.waiting.settle(mesh.pos, &accepted); - // A mesh from before a format change is remeshed anyway. let upload_secs: Vec<&SectionMesh> = mesh .sections @@ -1717,16 +1731,27 @@ impl ChunkBufferStore { .filter(|s| accepted.contains(&s.section_index)) .filter(|s| s.vertices.format() == self.format) .collect(); + let v_bytes: usize = upload_secs.iter().map(|s| s.vertices.bytes().len()).sum(); + let i_bytes: usize = upload_secs + .iter() + .map(|s| s.index_len() as usize * INDEX_SIZE as usize) + .sum(); + let (stg_v, stg_i) = self.staging_used[slot]; + if self.use_staging + && i > 0 + && (stg_v + v_bytes > staging_half || stg_i + i_bytes > staging_half) + { + // The rest go next frame, their waitlist claims still held. + taken = i; + break; + } + // Placed or starved again below, they no longer hold the room. + let unheld = self.waiting.settle(mesh.pos, &accepted); if self.use_staging && !upload_secs.is_empty() { // Verts and indices share the staging buffer (two halves). A chunk // too large for one half is dropped rather than overflowing the // buffer. This is permanent, so it's not reported for retry. - let v_bytes: usize = upload_secs.iter().map(|s| s.vertices.bytes().len()).sum(); - let i_bytes: usize = upload_secs - .iter() - .map(|s| s.index_len() as usize * INDEX_SIZE as usize) - .sum(); if v_bytes > staging_half || i_bytes > staging_half { tracing::warn!( "Chunk {:?} too large for staging ({} v / {} i bytes), skipping", @@ -1838,8 +1863,9 @@ impl ChunkBufferStore { .iter() .map(|p| p.index_len as usize * INDEX_SIZE as usize) .sum(); - // This mesh alone fits a half (checked above), so a flush always - // makes room. + // Only the batch's first mesh gets here with the half short + // (an edit uploaded earlier in the frame); it alone fits a half + // (checked above), so a flush always makes room. let (stg_v, stg_i) = self.staging_used[slot]; if stg_v + mv > staging_half || stg_i + mi > staging_half { self.flush_now(device); @@ -1905,13 +1931,13 @@ impl ChunkBufferStore { self.ensure_meta_capacity(device); - self.uploads(device) + self.uploads(device, taken) } /// The batch's starved sections, and those to retry: after the batch, so /// the retry sees the room it took and nothing it hands back is placed /// in the same batch. - fn uploads(&mut self, device: &rhi::backend::Device) -> ChunkUploads { + fn uploads(&mut self, device: &rhi::backend::Device, taken: usize) -> ChunkUploads { let mut remesh = Vec::new(); // Until something changes, the arenas are as full as before and the // retry would fail too. @@ -1921,6 +1947,7 @@ impl ChunkBufferStore { ChunkUploads { remesh, starved: std::mem::take(&mut self.starved), + taken, } } @@ -2655,7 +2682,7 @@ impl ChunkBufferStore { /// Push the CPU visibility graph's per-column visible-section masks. /// Columns not present default to fully visible, so the cull only omits /// sections the graph proved occluded. - pub fn set_chunk_visibility(&mut self, vis: HashMap) { + pub fn set_chunk_visibility(&mut self, vis: Arc>) { self.chunk_visibility = vis; for (slot, column) in self.lists.columns.iter() { self.column_visibility[slot as usize] = self @@ -2736,10 +2763,21 @@ impl ChunkBufferStore { } // This frame slot's GPU work has completed (fence-waited at frame start), - // so its readback holds the previous cull's totals (solid + cutout draws). - self.last_draw_count = self.count_readbacks[frame].read(|bytes| { - let (counts, _) = bytes.as_chunks::<4>(); - counts.iter().map(|&count| u32::from_ne_bytes(count)).sum() + // so its readback holds the previous cull's totals: per layer, the + // draws per arena then the indices per arena. + (self.last_draw_count, self.last_index_count) = self.count_readbacks[frame].read(|bytes| { + let words: &[u32] = bytemuck::cast_slice(bytes); + let (layers, _) = words.as_chunks::<{ 2 * MAX_ARENAS }>(); + let mut draws = 0; + let mut indices = 0; + for (layer, counts) in layers.iter().enumerate() { + let (per_arena, per_arena_indices) = counts.split_at(MAX_ARENAS); + if layer != Layer::Water as usize { + draws += per_arena.iter().sum::(); + } + indices += per_arena_indices.iter().sum::(); + } + (draws, indices) }); let data = self.frustum_data(frustum, anchor, eye, true); self.camera.cull( @@ -2751,12 +2789,12 @@ impl ChunkBufferStore { ); encoder.transition_buffers(rhi::BufferUse::StorageWrite, rhi::BufferUse::CopySrc); let readback = self.count_readbacks[frame].handle(); - for (i, count) in [ - &self.camera.counts[Layer::Solid as usize][frame], - &self.camera.counts[Layer::Cutout as usize][frame], - ] - .into_iter() - .enumerate() + for (i, count) in self + .camera + .counts + .iter() + .map(|counts| &counts[frame]) + .enumerate() { encoder.copy_buffer( count.handle(), @@ -3260,6 +3298,7 @@ impl DrawList { encoder.dispatch( pipeline, self.cull_sets[frame], + &[], [data.chunk_count.div_ceil(64), 1, 1], ); encoder.transition_buffers(rhi::BufferUse::StorageWrite, rhi::BufferUse::DrawInput); diff --git a/pomme-client/src/renderer/chunk/mesher.rs b/pomme-client/src/renderer/chunk/mesher.rs index 5399fcc85..f23e85149 100644 --- a/pomme-client/src/renderer/chunk/mesher.rs +++ b/pomme-client/src/renderer/chunk/mesher.rs @@ -11,7 +11,7 @@ use crate::renderer::chunk::atlas::{AtlasRegion, AtlasUVMap}; use crate::shaderpack::block_ids::{BlockIds, PackBlockIds}; use crate::world::block::model::{CardinalLighting, Direction, face_positions, face_uvs}; use crate::world::block::render_assets::{BlockRenderView, OwnedBlockRenderView, Tint}; -use crate::world::block::{BlockStateData, BlockStateTable, FluidKind}; +use crate::world::block::{BlockStateData, BlockStateTable, FaceMask, FluidKind}; use crate::world::chunk; use crate::world::chunk::{ChunkRing, ChunkStore, SectionBlocks}; @@ -991,6 +991,17 @@ impl MeshDispatcher { self.biome_climate = climate; } + /// How each block state looks in distant terrain, from this + /// dispatcher's textures, colormaps and biomes. + pub(crate) fn distant_colors(&self) -> Arc { + Arc::new(crate::renderer::distant::colors::DistantColors::new( + self.render_view.clone(), + Arc::clone(&self.uv_map), + Arc::clone(&self.biome_climate), + self.colormaps(), + )) + } + /// (grass, foliage, dry foliage) colormaps, shared with the particle /// store for break-particle tinting. pub fn colormaps(&self) -> (Arc, Arc, Arc) { @@ -1593,6 +1604,16 @@ impl BlockSlice { } } +impl ChunkStoreSnapshot { + /// Whether the block at `pos` draws its face toward `direction` + /// ([`should_render_face`] against the neighbour there). + fn face_shown(&self, pos: [i32; 3], direction: Direction) -> bool { + let offset = direction.offset(); + let neighbour = [pos[0] + offset[0], pos[1] + offset[1], pos[2] + offset[2]]; + should_render_face(self.state_data(pos), self.state_data(neighbour), direction) + } +} + impl LightSource for ChunkStoreSnapshot { fn state_data(&self, [x, y, z]: [i32; 3]) -> &BlockStateData { self.block_state_table @@ -1986,7 +2007,6 @@ fn emit_block_model( state, baked.quads.iter(), snapshot, - registry, uv_map, bx, by, @@ -1999,7 +2019,6 @@ fn emit_block_model( state, quads.iter().copied(), snapshot, - registry, uv_map, bx, by, @@ -2007,10 +2026,10 @@ fn emit_block_model( ); } else if let Some(textures) = registry.get_textures(state) { emit_cube_faces( - sink, model_pos, state, textures, snapshot, registry, uv_map, bx, by, bz, + sink, model_pos, state, textures, snapshot, uv_map, bx, by, bz, ); } else { - emit_missing_cube(sink, model_pos, snapshot, registry, uv_map, bx, by, bz); + emit_missing_cube(sink, model_pos, snapshot, uv_map, bx, by, bz); return false; } true @@ -2074,7 +2093,6 @@ fn emit_quads<'q>( state: azalea_block::BlockState, quads: impl Iterator + Clone, snapshot: &ChunkStoreSnapshot, - registry: BlockRenderView<'_>, uv_map: &AtlasUVMap, bx: i32, by: i32, @@ -2086,12 +2104,11 @@ fn emit_quads<'q>( .is_some_and(|quad| quad.ambient_occlusion); let smooth = smooth_lighting(snapshot, state, first_ao); for quad in quads { - if let Some(cullface) = quad.cullface { - let offset = cullface.offset(); - let neighbor = snapshot.get_block_state(bx + offset[0], by + offset[1], bz + offset[2]); - if registry.occludes_neighbor(neighbor) { - continue; - } + if quad + .cullface + .is_some_and(|cullface| !snapshot.face_shown([bx, by, bz], cullface)) + { + continue; } let region = uv_map.region(quad.texture); @@ -2124,7 +2141,6 @@ fn emit_cube_faces( state: BlockState, textures: &crate::world::block::render_assets::FaceTextures, snapshot: &ChunkStoreSnapshot, - registry: BlockRenderView<'_>, uv_map: &AtlasUVMap, bx: i32, by: i32, @@ -2134,9 +2150,7 @@ fn emit_cube_faces( let smooth = smooth_lighting(snapshot, state, true); for (i, dir) in CUBE_FACE_DIRS.iter().enumerate() { - let offset = dir.offset(); - let neighbor = snapshot.get_block_state(bx + offset[0], by + offset[1], bz + offset[2]); - if registry.occludes_neighbor(neighbor) { + if !snapshot.face_shown([bx, by, bz], *dir) { continue; } @@ -2224,6 +2238,64 @@ const MAX_FLUID_HEIGHT: f32 = 8.0 / 9.0; /// `FluidRenderer`'s `offs` anti-z-fighting inset. const FLUID_INSET: f32 = 0.001; +/// A block's occlusion shape on one of its faces (vanilla +/// `BlockStateBase.getFaceOcclusionShape`): the full face, nothing, or the +/// generated 16x16 mask. +enum FaceShape { + Full, + Empty, + Mask(&'static FaceMask), +} + +fn face_occlusion_shape(block_state_data: &BlockStateData, direction: Direction) -> FaceShape { + if block_state_data.is_air() || !block_state_data.light_props().can_occlude { + return FaceShape::Empty; + } + match block_state_data.light_props().face_occlusion { + Some(masks) => { + let mask = &masks[direction as usize]; + if mask.iter().all(|&row| row == 0xFFFF) { + FaceShape::Full + } else if mask.iter().all(|&row| row == 0) { + FaceShape::Empty + } else { + FaceShape::Mask(mask) + } + } + // TODO: masks are only dumped for useShapeForLightOcclusion states, so + // other partial occlusion shapes (fence posts, chests) count as empty. + None if block_state_data.outline_boxes().is_none() => FaceShape::Full, + None => FaceShape::Empty, + } +} + +/// Vanilla `Block.shouldRenderFace`: a quad facing `direction` is drawn +/// unless the neighbour's facing occlusion shape covers it, or the block +/// skips rendering against its own kind (`skipRendering`). +// TODO: `IronBarsBlock.skipRendering` (panes and bars hide the edge strip +// against a connected neighbour of their kind). +fn should_render_face( + block_state_data: &BlockStateData, + neighbour: &BlockStateData, + direction: Direction, +) -> bool { + let occluder = match face_occlusion_shape(neighbour, direction.opposite()) { + FaceShape::Full => return false, + FaceShape::Empty => None, + FaceShape::Mask(mask) => Some(mask), + }; + if block_state_data.culls_same(direction) && block_state_data.id() == neighbour.id() { + return false; + } + match (face_occlusion_shape(block_state_data, direction), occluder) { + (FaceShape::Mask(shape), Some(occluder)) => shape + .iter() + .zip(occluder) + .any(|(own, theirs)| own & !theirs != 0), + _ => true, + } +} + fn same_fluid(block_state_data: &BlockStateData, kind: FluidKind) -> bool { block_state_data.fluid().kind == kind } @@ -2235,23 +2307,17 @@ fn fluid_face_occluded_by_state( direction: Direction, height: f32, ) -> bool { - let props = block_state_data.light_props(); - if !props.can_occlude { - return false; - } - let full_row = |v: usize| match props.face_occlusion { - Some(masks) => masks[direction.opposite() as usize][v] == 0xFFFF, - // TODO: masks are only dumped for useShapeForLightOcclusion states, so - // other partial occlusion shapes (fence posts, chests) count as empty. - None => block_state_data.outline_boxes().is_none(), - }; let rows = match direction { // `Shapes.blockOccludes` needs the fluid box to reach the top boundary. Direction::Up if (height - 1.0).abs() > 1.0e-7 => return false, Direction::Up | Direction::Down => 16, _ => ((height.clamp(0.0, 1.0) * 16.0).ceil() as usize).min(16), }; - (0..rows).all(full_row) + match face_occlusion_shape(block_state_data, direction.opposite()) { + FaceShape::Full => true, + FaceShape::Empty => false, + FaceShape::Mask(mask) => mask[..rows].iter().all(|&row| row == 0xFFFF), + } } fn fluid_face_occluded_by_self(block_state_data: &BlockStateData, direction: Direction) -> bool { @@ -2629,7 +2695,6 @@ fn emit_missing_cube( sink: &mut MeshSink, block_pos: [f32; 3], snapshot: &ChunkStoreSnapshot, - registry: BlockRenderView<'_>, uv_map: &AtlasUVMap, bx: i32, by: i32, @@ -2637,9 +2702,7 @@ fn emit_missing_cube( ) { let missing = uv_map.missing_region(); for dir in &CUBE_FACE_DIRS { - let offset = dir.offset(); - let neighbor = snapshot.get_block_state(bx + offset[0], by + offset[1], bz + offset[2]); - if registry.occludes_neighbor(neighbor) { + if !snapshot.face_shown([bx, by, bz], *dir) { continue; } @@ -2753,6 +2816,52 @@ pub(crate) fn cube_face_geometry(dir: Direction) -> ([[f32; 3]; 4], [[f32; 2]; 4 ) } +#[cfg(test)] +mod face_tests { + use super::should_render_face; + use crate::world::block::BlockStateTable; + use crate::world::block::model::Direction; + + #[test] + fn faces_hide_as_vanilla_should_render_face_decides() { + let table = BlockStateTable::native(); + let state = + |name, props: &[(&str, &str)]| table.block_state_data(table.find_state(name, props)); + let stone = state("stone", &[]); + let ice = state("ice", &[]); + let glass = state("glass", &[]); + let one_layer = state("snow", &[("layers", "1")]); + let three_layers = state("snow", &[("layers", "3")]); + let bottom_slab = state("oak_slab", &[("type", "bottom"), ("waterlogged", "false")]); + let fence = state("oak_fence", &[("waterlogged", "false")]); + let air = state("air", &[]); + + // A full occluding neighbour hides the face; air and glass don't. + assert!(!should_render_face(stone, stone, Direction::North)); + assert!(should_render_face(stone, air, Direction::North)); + assert!(should_render_face(stone, glass, Direction::North)); + // Half-transparent blocks skip their own kind only. + assert!(!should_render_face(ice, ice, Direction::North)); + assert!(should_render_face(ice, glass, Direction::North)); + assert!(should_render_face(glass, ice, Direction::North)); + // Partial shapes compare their face masks: equal snow layers hide + // each other's sides, a taller one shows above a shorter one, and + // a slab's full bottom hides the block below's top. + assert!(!should_render_face(one_layer, one_layer, Direction::East)); + assert!(!should_render_face( + one_layer, + three_layers, + Direction::East + )); + assert!(should_render_face(three_layers, one_layer, Direction::East)); + assert!(should_render_face(one_layer, air, Direction::Up)); + assert!(!should_render_face(stone, bottom_slab, Direction::Up)); + assert!(should_render_face(stone, bottom_slab, Direction::North)); + // No masks for a fence post: its neighbours' faces stay. + assert!(should_render_face(stone, fence, Direction::North)); + } +} + #[cfg(test)] mod fluid_height_tests { use super::{ diff --git a/pomme-client/src/renderer/chunk/mod.rs b/pomme-client/src/renderer/chunk/mod.rs index f262b9a98..cbc7c055d 100644 --- a/pomme-client/src/renderer/chunk/mod.rs +++ b/pomme-client/src/renderer/chunk/mod.rs @@ -6,4 +6,4 @@ pub mod mesh_source; pub mod mesher; pub mod moving_blocks; pub mod occlusion_graph; -mod tlsf; +pub(crate) mod tlsf; diff --git a/pomme-client/src/renderer/chunk/occlusion_graph.rs b/pomme-client/src/renderer/chunk/occlusion_graph.rs index 5b800b9af..020840843 100644 --- a/pomme-client/src/renderer/chunk/occlusion_graph.rs +++ b/pomme-client/src/renderer/chunk/occlusion_graph.rs @@ -2,10 +2,13 @@ //! 6x6 face-to-face connectivity set at mesh time (vanilla's `VisGraph` flood //! fill); the per-frame occlusion walk (`SectionOcclusionGraph`) consumes it. -use std::collections::{HashMap, VecDeque}; +use std::collections::{HashMap, HashSet, VecDeque}; +use std::sync::Arc; +use crossbeam_channel::{Receiver, Sender}; use glam::DVec3; +use super::buffer::column_is_near; use crate::position::ChunkPos; /// The six block faces, ordinals matching vanilla `Direction` @@ -356,6 +359,263 @@ fn ray_occluded( false } +/// Extra FOV (radians) for the tier-1 "about to be seen" margin frustum, so +/// small camera turns reveal already-meshed terrain instead of a meshing +/// curtain. +pub const VIS_MARGIN_RADIANS: f32 = 0.6; + +/// Frustum tier for a column: 0 in view, 1 in the dilated margin, 2 behind the +/// camera. (Nearby columns are forced to 0 by the caller.) +fn column_frustum_tier( + pos: ChunkPos, + eye: DVec3, + planes: &[[f32; 4]; 6], + planes_wide: &[[f32; 4]; 6], + min_y: f32, + max_y: f32, +) -> u8 { + // Camera-relative full-height column box, matching how the GPU cull + // subtracts the eye before its plane test (cull.comp); f64 first for + // precision at extreme coordinates. + let dx = (pos.x as f64 * 16.0 - eye.x) as f32; + let dz = (pos.z as f64 * 16.0 - eye.z) as f32; + let mn = [dx, (min_y as f64 - eye.y) as f32, dz]; + let mx = [dx + 16.0, (max_y as f64 - eye.y) as f32, dz + 16.0]; + if aabb_in_frustum(&mn, &mx, planes) { + 0 + } else if aabb_in_frustum(&mn, &mx, planes_wide) { + 1 + } else { + 2 + } +} + +/// Full mask for an `n`-section column (bits `0..n` set). +pub fn section_mask(n: i32) -> u32 { + if n >= 32 { u32::MAX } else { (1u32 << n) - 1 } +} + +/// Conservative AABB-vs-frustum test (the dominant-corner max-dot used by +/// `cull.comp`): true unless the box is fully behind some plane. +fn aabb_in_frustum(mn: &[f32; 3], mx: &[f32; 3], planes: &[[f32; 4]; 6]) -> bool { + for p in planes { + let d = p[0] * if p[0] >= 0.0 { mx[0] } else { mn[0] } + + p[1] * if p[1] >= 0.0 { mx[1] } else { mn[1] } + + p[2] * if p[2] >= 0.0 { mx[2] } else { mn[2] } + + p[3]; + if d < 0.0 { + return false; + } + } + true +} + +/// What the walk thread is told about the world: a section's visibility set +/// from its mesh, a column loading or unloading, the meshes or the level gone. +pub enum Update { + Section { + pos: ChunkPos, + si: i32, + vis: VisibilitySet, + }, + Load(ChunkPos), + Unload { + pos: ChunkPos, + sections: i32, + }, + /// The meshes went: every section's set goes, the columns stay loaded. + ClearSections, + /// The level went, columns and all. + Clear, +} + +/// One walk from the camera, applied against the frustum it was asked with. +pub struct Walk { + /// Matched against the result's, so a walk asked before the level was + /// cleared is dropped. + pub generation: u64, + pub cam_col: ChunkPos, + pub eye: DVec3, + pub planes: [[f32; 4]; 6], + pub planes_wide: [[f32; 4]; 6], + pub min_y: i32, + pub section_count: i32, + /// Whether occluded sections are left out of what the renderer draws. + pub occlusion: bool, +} + +/// A walk's outcome, per loaded column. +pub struct Visibility { + pub generation: u64, + /// What the renderer draws: bit `si` set for a section not occluded, or + /// every section with occlusion off. + pub draw: Arc>, + /// The in-frustum columns with a visible section, and their masks. + pub in_view: HashMap, +} + +impl Walk { + fn run( + &self, + section_vis: &HashMap<(ChunkPos, i32), VisibilitySet>, + columns: &HashSet, + ) -> Visibility { + let cam_si = ((self.eye.y - self.min_y as f64) / 16.0).floor() as i32; + // Bound the walk by the loaded radius (a server can stream terrain + // past the client render distance). + let rd = columns + .iter() + .map(|p| { + (p.x - self.cam_col.x) + .abs() + .max((p.z - self.cam_col.z).abs()) + }) + .max() + .unwrap_or(0); + let bfs = compute_visible_mask( + section_vis, + self.cam_col, + cam_si, + self.eye, + self.min_y, + self.section_count, + rd, + ); + let min_y = self.min_y as f32; + let max_y = min_y + (self.section_count * 16) as f32; + let full = section_mask(self.section_count); + let mut in_view = HashMap::new(); + let mut masks = HashMap::with_capacity(columns.len()); + for &pos in columns { + let near = column_is_near(pos, self.eye); + // Near columns always draw fully; otherwise a column draws only the + // sections the graph proved occlusion-visible (none => fully hidden). + let mask = if near { + full + } else { + bfs.get(&pos).copied().unwrap_or(0) + }; + if mask != 0 + && (near + || column_frustum_tier( + pos, + self.eye, + &self.planes, + &self.planes_wide, + min_y, + max_y, + ) == 0) + { + in_view.insert(pos, mask); + } + masks.insert(pos, mask); + } + let draw = if self.occlusion { + Arc::new(masks) + } else { + Arc::new(columns.iter().map(|&pos| (pos, full)).collect()) + }; + Visibility { + generation: self.generation, + draw, + in_view, + } + } +} + +enum Message { + Update(Update), + Walk(Box), +} + +/// The thread that owns the section visibility sets and walks them. Updates +/// and walks arrive in order, so a walk sees every update sent before it, +/// and the game thread never copies the sets. Vanilla's +/// `SectionOcclusionGraph` runs its walk on a thread the same way. +pub struct Walker { + tx: Sender, + rx: Receiver, +} + +/// The thread only ends early by panicking; the game goes down with it. +const THREAD_GONE: &str = "the chunk occlusion thread panicked"; + +impl Walker { + pub fn spawn() -> Self { + let (tx, messages) = crossbeam_channel::unbounded(); + let (results, rx) = crossbeam_channel::unbounded(); + std::thread::Builder::new() + .name("chunk-occlusion".into()) + .spawn(move || run(&messages, &results)) + .expect("failed to spawn the chunk occlusion thread"); + Self { tx, rx } + } + + pub fn update(&self, update: Update) { + self.tx.send(Message::Update(update)).expect(THREAD_GONE); + } + + pub fn walk(&self, walk: Walk) { + self.tx + .send(Message::Walk(Box::new(walk))) + .expect(THREAD_GONE); + } + + pub fn try_result(&self) -> Option { + crate::util::try_recv_live(&self.rx, THREAD_GONE) + } +} + +/// The walk thread's view of the world. +#[derive(Default)] +struct WalkState { + section_vis: HashMap<(ChunkPos, i32), VisibilitySet>, + columns: HashSet, +} + +impl WalkState { + fn apply(&mut self, update: Update) { + match update { + Update::Section { pos, si, vis } => { + self.section_vis.insert((pos, si), vis); + } + Update::Load(pos) => { + self.columns.insert(pos); + } + Update::Unload { pos, sections } => { + self.columns.remove(&pos); + for si in 0..sections { + self.section_vis.remove(&(pos, si)); + } + } + Update::ClearSections => self.section_vis.clear(), + Update::Clear => { + self.section_vis.clear(); + self.columns.clear(); + } + } + } + + fn walk(&self, walk: &Walk) -> Visibility { + walk.run(&self.section_vis, &self.columns) + } +} + +/// Ends when the game drops its [`Walker`]. +fn run(messages: &Receiver, results: &Sender) { + let mut state = WalkState::default(); + for message in messages { + match message { + Message::Update(update) => state.apply(update), + Message::Walk(walk) => { + if results.send(state.walk(&walk)).is_err() { + break; + } + } + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -425,4 +685,47 @@ mod tests { // ...but the section directly behind it on the same row/height is not. assert!(mask.get(&ChunkPos::new(0, -2)).copied().unwrap_or(0) & (1 << 4) == 0); } + + fn far_walk() -> Walk { + // Every plane passes everything, so only occlusion decides. + let open = [[0.0, 0.0, 0.0, 1.0]; 6]; + Walk { + generation: 0, + cam_col: ChunkPos::new(0, 0), + eye: DVec3::new(8.0, 72.0, 8.0), + planes: open, + planes_wide: open, + min_y: 0, + section_count: 8, + occlusion: true, + } + } + + #[test] + #[should_panic(expected = "occlusion thread panicked")] + fn a_dead_thread_takes_the_game_down() { + let (tx, _messages) = crossbeam_channel::unbounded(); + let (results, rx) = crossbeam_channel::unbounded::(); + drop(results); + Walker { tx, rx }.try_result(); + } + + #[test] + fn clearing_sections_keeps_loaded_columns() { + let pos = ChunkPos::new(6, 0); + let mut state = WalkState::default(); + state.apply(Update::Load(ChunkPos::new(0, 0))); + state.apply(Update::Load(pos)); + state.apply(Update::Section { + pos, + si: 4, + vis: VisibilitySet::none(), + }); + state.apply(Update::ClearSections); + let vis = state.walk(&far_walk()); + assert_eq!(vis.draw.get(&pos), Some(§ion_mask(8))); + + state.apply(Update::Clear); + assert!(state.walk(&far_walk()).draw.is_empty()); + } } diff --git a/pomme-client/src/renderer/distant/bakery.rs b/pomme-client/src/renderer/distant/bakery.rs new file mode 100644 index 000000000..3ca25693c --- /dev/null +++ b/pomme-client/src/renderer/distant/bakery.rs @@ -0,0 +1,581 @@ +//! Distant terrain's model texture bakery: a block state's model drawn from +//! each of its six sides into a 16x16 face texture on the CPU, colour, depth +//! and which texels are tinted. + +use azalea_block::BlockState; +use glam::{Mat4, Quat, Vec3, Vec4}; + +use crate::renderer::chunk::atlas::{AtlasUVMap, SpritePixels}; +use crate::world::block::FluidKind; +use crate::world::block::model::{BakedQuad, Direction, face_positions, face_uvs}; +use crate::world::block::render_assets::{BlockRenderView, Tint}; + +/// Each face texture's side, in texels. +pub(crate) const MODEL_TEXTURE_SIZE: usize = 16; +pub(crate) const TEXELS: usize = MODEL_TEXTURE_SIZE * MODEL_TEXTURE_SIZE; + +const INTEGER_BITS: u32 = 13; +const TOTAL_INTEGER_BITS: u32 = INTEGER_BITS + 1; +const FIXED_POINT_BITS: u32 = 32 - TOTAL_INTEGER_BITS; +const FIXED_POINT_BIT_SCALE: i64 = (1 << FIXED_POINT_BITS) - 1; + +const DEPTH_MASK: u64 = ((1 << 24) - 1) << (64 - 24); +/// Depth at its farthest, the rest zero. +const CLEAR_VALUE: u64 = DEPTH_MASK; + +/// Quad meta: texels with no alpha are left out. +const META_DISCARD: u32 = 1; +/// Quad meta: the texels take the block's tint. +const META_TINTED: u32 = 4; + +/// One face's bake: per texel, bottom row first, its ABGR colour and its +/// depth word (24-bit depth high, bit 7 tinted, the low bits how many quads +/// covered it). +#[derive(Clone, PartialEq, Eq, Hash)] +pub(crate) struct FaceTexture { + pub colour: Box<[u32; TEXELS]>, + pub depth: Box<[u32; TEXELS]>, +} + +/// A model quad as the rasterizer takes it: block-local corners, sprite +/// UVs and the sprite they sample. +struct Quad<'a> { + positions: [Vec3; 4], + uvs: [[f32; 2]; 4], + meta: u32, + sprite: &'a SpritePixels, +} + +/// A software rasterizer: a 16x16 target of 64-bit texels, colour in the +/// low 32 bits, a stencil count, the tint bit and a 24-bit depth in the +/// high. +struct Rasterizer { + framebuffer: [u64; TEXELS], + cull_back_face: bool, + blending: bool, +} + +/// A transformed corner: screen position (fixed point) and its attributes. +#[derive(Clone, Copy, Default)] +struct Corner { + x: i32, + y: i32, + z: i32, + meta: u32, + u: f32, + v: f32, +} + +impl Rasterizer { + fn new() -> Self { + Self { + framebuffer: [CLEAR_VALUE; TEXELS], + cull_back_face: false, + blending: false, + } + } + + fn clear(&mut self) { + self.framebuffer.fill(CLEAR_VALUE); + } + + fn raster(&mut self, mvp: &Mat4, quads: &[Quad]) { + for quad in quads { + self.raster_quad(mvp, quad); + } + } + + fn raster_quad(&mut self, transform: &Mat4, quad: &Quad) { + let corners: [Corner; 4] = std::array::from_fn(|i| load_transform_pos(transform, quad, i)); + // 0, 1, 2 | 2, 3, 0 + self.raster_triangle(quad.sprite, [corners[0], corners[1], corners[2]], false); + self.raster_triangle(quad.sprite, [corners[2], corners[3], corners[0]], true); + } + + fn raster_triangle(&mut self, sprite: &SpritePixels, [v1, v2, v3]: [Corner; 3], or_zero: bool) { + let area = edge(&v1, &v2, v3.x, v3.y); + // A negative area winds counterclockwise. + if (area < 0) == self.cull_back_face { + return; + } + if from_fixed(area).abs() < 0.001 { + return; + } + let last = to_fixed_int(MODEL_TEXTURE_SIZE as i32 - 1); + let min_x = from_fixed_to_int(v1.x.min(v2.x).min(v3.x).max(0)); + let max_x = from_fixed_to_int(v1.x.max(v2.x).max(v3.x).min(last)); + let min_y = from_fixed_to_int(v1.y.min(v2.y).min(v3.y).max(0)); + let max_y = from_fixed_to_int(v1.y.max(v2.y).max(v3.y).min(last)); + for py in min_y..=max_y { + for px in min_x..=max_x { + let cx = to_fixed_int(px) + to_fixed(0.5); + let cy = to_fixed_int(py) + to_fixed(0.5); + let w1 = fixed_div(edge(&v2, &v3, cx, cy), area); + let w2 = fixed_div(edge(&v3, &v1, cx, cy), area); + let w3 = to_fixed(1.0) - w1 - w2; + let inside = w1 > 0 && w2 > 0 && w3 > 0; + if inside || (or_zero && w1 >= 0 && w2 >= 0 && w3 >= 0) { + let (b1, b2, b3) = (from_fixed(w1), from_fixed(w2), from_fixed(w3)); + let z = b1.mul_add( + from_fixed(v1.z), + b2.mul_add(from_fixed(v2.z), b3 * from_fixed(v3.z)), + ); + let index = (px + py * MODEL_TEXTURE_SIZE as i32) as usize; + self.raster_pixel(sprite, index, [b1, b2, b3], [v1, v2, v3], z); + } + } + } + } + + fn raster_pixel( + &mut self, + sprite: &SpritePixels, + index: usize, + [b1, b2, b3]: [f32; 3], + [a1, a2, a3]: [Corner; 3], + z: f32, + ) { + let mut z = z.mul_add(0.5, 0.5); + if (-0.000_001..0.0).contains(&z) { + z = 0.0; + } + if !(0.0..=1.0).contains(&z) { + return; + } + let meta = a1.meta; + let u = b1.mul_add(a1.u, b2.mul_add(a2.u, b3 * a3.u)); + let v = b1.mul_add(a1.v, b2.mul_add(a2.v, b3 * a3.v)); + let mut colour = sample(sprite, u, v); + if meta & META_DISCARD != 0 && colour >> 24 == 0 { + return; + } + let texel = &mut self.framebuffer[index]; + // The stencil counts first. + *texel = texel.wrapping_add(1 << 32); + let mut depth = ((f64::from(z) * f64::from((1 << 24) - 1)) as u64) << (64 - 24); + if depth == DEPTH_MASK { + // Something shows even at the far plane. + depth -= 1; + } + // Strictly less than. + if *texel <= depth { + return; + } + *texel = (*texel & !DEPTH_MASK) | depth; + *texel = (*texel & !(1 << 39)) | u64::from(meta & META_TINTED) << 37; + let below = *texel as u32; + *texel &= !u64::from(u32::MAX); + if self.blending { + colour = blend(below, colour); + } + *texel |= u64::from(colour); + } +} + +/// The nearest texel of `sprite` at `u`, `v`, as ABGR. +fn sample(sprite: &SpritePixels, u: f32, v: f32) -> u32 { + let (w, h) = (sprite.width as i32, sprite.height as i32); + // Java's `Math.round(u * w - 0.5)`. + let pu = ((u * w as f32).floor() as i32).clamp(0, w - 1); + let pv = ((v * h as f32).floor() as i32).clamp(0, h - 1); + let at = (pv * w + pu) as usize * 4; + u32::from_le_bytes(sprite.rgba[at..at + 4].try_into().expect("four bytes")) +} + +/// `top` blended over `below`, both ABGR. +fn blend(below: u32, top: u32) -> u32 { + let below_alpha = below >> 24; + if below_alpha == 0 { + return top; + } + let top_alpha = top >> 24; + let alpha = (below_alpha + ((top_alpha * (255 - below_alpha)) >> 8)).min(0xFF); + mix(top & 0xFF_FFFF, below & 0xFF_FFFF, top_alpha) | alpha << 24 +} + +/// Sodium's `ColorMixer.mix`: `start * weight + end * (255 - weight)` per +/// channel, rounded. +fn mix(start: u32, end: u32, weight: u32) -> u32 { + (0..4).fold(0, |out, i| { + let (a, b) = ((start >> (i * 8)) & 0xFF, (end >> (i * 8)) & 0xFF); + out | ((a * weight + b * (255 - weight) + 127) / 255) << (i * 8) + }) +} + +fn load_transform_pos(transform: &Mat4, quad: &Quad, vert: usize) -> Corner { + let pos = *transform * quad.positions[vert].extend(1.0); + assert!((pos.w - 1.0).abs() <= 0.000_001, "the views are affine"); + let size = MODEL_TEXTURE_SIZE as f32; + let [u, v] = quad.uvs[vert]; + Corner { + x: to_fixed(pos.x.mul_add(0.5, 0.5) * size), + y: to_fixed(pos.y.mul_add(0.5, 0.5) * size), + z: to_fixed(pos.z), + meta: quad.meta, + u, + v, + } +} + +fn edge(a: &Corner, b: &Corner, cx: i32, cy: i32) -> i32 { + fixed_mul(cx - a.x, b.y - a.y) - fixed_mul(cy - a.y, b.x - a.x) +} + +fn to_fixed(a: f32) -> i32 { + (f64::from(a) * FIXED_POINT_BIT_SCALE as f64) as i32 +} + +fn to_fixed_int(a: i32) -> i32 { + (i64::from(a) * FIXED_POINT_BIT_SCALE) as i32 +} + +fn from_fixed(a: i32) -> f32 { + (f64::from(a) / FIXED_POINT_BIT_SCALE as f64) as f32 +} + +fn from_fixed_to_int(a: i32) -> i32 { + (i64::from(a) / FIXED_POINT_BIT_SCALE) as i32 +} + +fn fixed_mul(a: i32, b: i32) -> i32 { + (i64::from(a) * i64::from(b) / FIXED_POINT_BIT_SCALE) as i32 +} + +fn fixed_div(a: i32, b: i32) -> i32 { + (i64::from(a) * FIXED_POINT_BIT_SCALE / i64::from(b)) as i32 +} + +/// The six views, by face index ([`Direction::VALUES`]): each looks at the +/// block from that side and maps it to clip space, the face's texels at `x` +/// and `y`. +fn views() -> [Mat4; 6] { + [ + view(-90.0, 0.0, 0.0, 0), + view(90.0, 0.0, 0.0, 0b100), + view(0.0, 180.0, 0.0, 0b001), + view(0.0, 0.0, 0.0, 0), + view(0.0, 90.0, 270.0, 0b100), + view(0.0, 270.0, 270.0, 0), + ] +} + +/// One view: the pose (about the block's centre, flipped per `flip`), then +/// the orthographic projection of the unit cube. +fn view(pitch: f32, yaw: f32, rotation: f32, flip: i32) -> Mat4 { + let scale = Vec3::new( + (1 - 2 * (flip & 1)) as f32, + (1 - (flip & 2)) as f32, + (1 - ((flip >> 1) & 2)) as f32, + ); + let pose = Mat4::from_translation(Vec3::splat(0.5)) + * Mat4::from_quat(quat(Vec3::Z, rotation)) + * Mat4::from_quat(quat(Vec3::X, pitch)) + * Mat4::from_quat(quat(Vec3::Y, yaw)) + * Mat4::from_scale(scale) + * Mat4::from_translation(Vec3::splat(-0.5)); + let ortho = Mat4::from_cols( + Vec4::new(2.0, 0.0, 0.0, 0.0), + Vec4::new(0.0, 2.0, 0.0, 0.0), + Vec4::new(0.0, 0.0, -2.0, 0.0), + Vec4::new(-1.0, -1.0, 1.0, 1.0), + ); + ortho * pose +} + +/// A rotation of `angle` degrees about `axis`, its sine and cosine taken in +/// `f64`. +fn quat(axis: Vec3, angle: f32) -> Quat { + let half = angle.to_radians() / 2.0; + let sin = f64::from(half).sin() as f32; + let axis = axis / axis.length(); + Quat::from_xyzw( + axis.x * sin, + axis.y * sin, + axis.z * sin, + f64::from(half).cos() as f32, + ) +} + +/// What a bake found across the model's quads. +#[derive(Clone, Copy)] +pub(crate) struct BakeFlags { + /// Some quad's sprite mips as `dark_cutout`. + pub darkened: bool, + /// Some quad is in the translucent layer. + pub translucent: bool, + /// Some opaque-layer quad leaves texels out. + pub discard: bool, + /// Some quad takes its face's directional shade, so the model shades + /// per face. Fluids do too, as vanilla's `FluidRenderer` shades them, + /// where the reference leaves them unshaded. + pub shaded: bool, + /// The first tinted quad's tint. + pub tint: Tint, +} + +/// Whether `quad` takes directional shade. +fn shades(quad: &BakedQuad) -> bool { + quad.shade_face.is_some() +} + +/// The rasterizer and the six views every block state is drawn through. +pub(crate) struct Bakery { + rasterizer: Rasterizer, + views: [Mat4; 6], +} + +impl Bakery { + pub fn new() -> Self { + Self { + rasterizer: Rasterizer::new(), + views: views(), + } + } + + /// `state`'s six faces, by face index, or `None` when it draws nothing. + pub fn render<'a>( + &mut self, + view: BlockRenderView<'a>, + uv_map: &'a AtlasUVMap, + state: BlockState, + ) -> Option<([FaceTexture; 6], BakeFlags)> { + let data = view.block_state_table().block_state_data(state); + // Fluids draw through vanilla's `FluidRenderer`, their block being + // invisible. + if data.is_invisible() && !data.is_fluid_block() { + return None; + } + // Leaves go in the solid layer, their clear texels kept. + let leaves = data.is_leaves(); + let mut flags = BakeFlags { + darkened: false, + translucent: false, + discard: false, + shaded: false, + tint: Tint::None, + }; + let (mut opaque, mut translucent): (Vec>, Vec>) = + (Vec::new(), Vec::new()); + let mut add = |quad: Quad<'a>, layer_translucent: bool, opaque_texels: bool| { + flags.darkened |= quad.sprite.dark_cutout; + if layer_translucent { + flags.translucent = true; + translucent.push(Quad { + meta: quad.meta | META_DISCARD, + ..quad + }); + } else { + let discard = !leaves && !opaque_texels; + flags.discard |= discard; + opaque.push(Quad { + meta: quad.meta | if discard { META_DISCARD } else { 0 }, + ..quad + }); + } + }; + if data.is_fluid_block() { + let fluid = data.fluid().kind; + flags.shaded = true; + if fluid == FluidKind::Water { + flags.tint = Tint::Water; + } + for quad in fluid_quads(uv_map, fluid) { + add(quad, fluid == FluidKind::Water, true); + } + } else { + let quads: Vec<&BakedQuad> = match view.get_baked_model(state) { + Some(model) => model.quads.iter().collect(), + None => view.get_multipart_quads(state).unwrap_or_default(), + }; + for quad in quads { + let name = quad.texture.name(); + let Some(sprite) = uv_map.sprite_pixels(name) else { + continue; + }; + flags.shaded |= shades(quad); + if flags.tint == Tint::None { + flags.tint = quad.tint; + } + let region = uv_map.region(quad.texture); + let meta = if quad.tint == Tint::None { + 0 + } else { + META_TINTED + }; + add( + Quad { + positions: quad.positions.map(Vec3::from), + uvs: quad.uvs, + meta, + sprite, + }, + region.translucent, + region.opaque, + ); + } + } + if opaque.is_empty() && translucent.is_empty() { + return None; + } + let faces = std::array::from_fn(|i| { + let r = &mut self.rasterizer; + r.cull_back_face = i == 1 || i == 2 || i == 4; + r.clear(); + r.blending = false; + r.raster(&self.views[i], &opaque); + r.blending = true; + r.raster(&self.views[i], &translucent); + FaceTexture { + colour: Box::new(r.framebuffer.map(|texel| texel as u32)), + depth: Box::new(r.framebuffer.map(|texel| (texel >> 32) as u32)), + } + }); + Some((faces, flags)) + } +} + +/// A full block of `fluid`: still on top and bottom, flowing on the sides +/// over vanilla's half of the sprite, as vanilla's `FluidRenderer` draws a +/// source with nothing around it. Departure: the top stays at the block's +/// top where `FluidRenderer` lowers it, so a run of water voxels meets +/// without a step; the surface's own height comes from the voxel's gap +/// (`ModelFactory::fluid_surface`). +fn fluid_quads(uv_map: &AtlasUVMap, fluid: FluidKind) -> Vec> { + let (still, flow, meta) = match fluid { + FluidKind::Water => ("water_still", "water_flow", META_TINTED), + _ => ("lava_still", "lava_flow", 0), + }; + let (from, to) = ([0.0; 3], [1.0; 3]); + Direction::VALUES + .into_iter() + .filter_map(|dir| { + let (name, uv) = match dir { + Direction::Up | Direction::Down => (still, [0.0, 0.0, 16.0, 16.0]), + _ => (flow, [0.0, 0.0, 8.0, 8.0]), + }; + Some(Quad { + positions: face_positions(dir, from, to).map(Vec3::from), + uvs: face_uvs(dir, from, to, Some(&uv), None), + meta, + sprite: uv_map.sprite_pixels(name)?, + }) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use super::*; + + fn sprite(rgba: [u8; 4]) -> SpritePixels { + SpritePixels { + width: 1, + height: 1, + rgba: Arc::from(rgba.as_slice()), + dark_cutout: false, + } + } + + /// The unit cube's `dir` face. + fn face(dir: Direction, sprite: &SpritePixels) -> Quad<'_> { + let (from, to) = ([0.0; 3], [1.0; 3]); + Quad { + positions: face_positions(dir, from, to).map(Vec3::from), + uvs: face_uvs(dir, from, to, None, None), + meta: 0, + sprite, + } + } + + fn bake(quads: &[Quad], view: usize) -> [u64; TEXELS] { + let mut r = Rasterizer::new(); + r.cull_back_face = view == 1 || view == 2 || view == 4; + r.raster(&views()[view], quads); + r.framebuffer + } + + #[test] + fn each_view_sees_its_face_of_a_cube_and_nothing_else() { + let colours = [[1, 0, 0, 255], [2, 0, 0, 255], [3, 0, 0, 255]]; + let sprites = colours.map(sprite); + let dirs = Direction::VALUES; + for (view, &dir) in dirs.iter().enumerate() { + let quads: Vec = dirs + .iter() + .map(|&other| face(other, &sprites[usize::from(other == dir)])) + .collect(); + let texels = bake(&quads, view); + assert!( + texels + .iter() + .all(|&t| t as u32 == u32::from_le_bytes([2, 0, 0, 255])), + "view {view} shows its own face over the whole texture" + ); + } + } + + #[test] + fn the_nearer_quad_wins_and_blending_puts_the_top_over() { + let (red, blue) = (sprite([255, 0, 0, 255]), sprite([0, 0, 255, 128])); + let mut back = face(Direction::North, &red); + let mut front = face(Direction::North, &blue); + // The north view looks at z = 0 from outside: nearer is lower z. + for p in &mut back.positions { + p.z = 0.5; + } + for p in &mut front.positions { + p.z = 0.25; + } + let texels = bake(&[back, front], 2); + assert_eq!(texels[0] as u32, u32::from_le_bytes([0, 0, 255, 128])); + assert_eq!(blend(0xFF00_00FF, 0x8000_00FF) >> 24, 255); + assert_eq!(blend(0, 0x8012_3456), 0x8012_3456); + } + + /// Per view, the block axes a face texture's `x` and `y` run along: + /// every view maps them one to one, unflipped. `distant.vert` mirrors + /// this. + const TILE_AXES: [[usize; 2]; 6] = [[0, 2], [0, 2], [0, 1], [0, 1], [1, 2], [1, 2]]; + + #[test] + fn tile_axes_follow_the_views() { + for (view, axes) in views().iter().zip(TILE_AXES) { + let rows = view.transpose(); + for (row, axis) in [rows.x_axis, rows.y_axis].into_iter().zip(axes) { + // Clip space to texels, in blocks. + let texel = row * 0.5 + Vec4::W * 0.5; + assert!(texel.abs_diff_eq(Vec3::AXES[axis].extend(0.0), 1e-6)); + } + } + } + + #[test] + fn shade_follows_the_element_not_ambient_occlusion() { + let quad = |shade_face, ambient_occlusion| BakedQuad { + positions: face_positions(Direction::Up, [0.0; 3], [1.0; 3]), + uvs: [[0.0; 2]; 4], + texture: crate::world::block::model::TextureId::intern("block/stone"), + cullface: None, + tint: Tint::None, + tint_index: None, + shade_light: 1.0, + shade_face, + emission: 0, + ambient_occlusion, + }; + assert!(shades(&quad(Some(Direction::Up), false))); + assert!(!shades(&quad(None, true))); + } + + #[test] + fn discarded_texels_leave_nothing() { + let clear = sprite([9, 9, 9, 0]); + let quad = Quad { + meta: META_DISCARD, + ..face(Direction::Up, &clear) + }; + assert!(bake(&[quad], 1).iter().all(|&t| t == CLEAR_VALUE)); + } +} diff --git a/pomme-client/src/renderer/distant/build.rs b/pomme-client/src/renderer/distant/build.rs new file mode 100644 index 000000000..0da5a2300 --- /dev/null +++ b/pomme-client/src/renderer/distant/build.rs @@ -0,0 +1,1348 @@ +//! The section mesher: a world section meshed into quads, a pass per axis +//! over its opaque blocks, its fluids and its other blocks, each face culled +//! against what it touches, inside the section and in the six around it. + +use super::models::{Metadata, Models}; +use super::scan::{ + GROUPS, Mesher, QUAD_BIOME_SHIFT, QUAD_LIGHT, QUAD_LIGHT_SHIFT, QUAD_MODEL_SHIFT, Quads, +}; +use super::section::{self, SectionKey, UNSEEN, VOLUME, WorldSection}; +use super::voxel; + +/// Model ids in a quad, where the mesher compares them. +const MODEL: u64 = 0xFFFF << QUAD_MODEL_SHIFT; +/// A quad's block light. +const BLOCK_LIGHT: u64 = 0xF << (QUAD_LIGHT_SHIFT + 4); + +pub(crate) struct BuiltSection { + pub key: SectionKey, + /// [`WorldSection::non_empty_children`] when it was meshed. + pub child_existence: u8, + /// Its quads' bounds: min x, y, z in 5 bits each, then the extents less + /// one in 5 bits each from bit 15. + pub aabb: u32, + /// Every group's quads in turn ([`GROUPS`]); empty for a section with + /// nothing to draw. + pub quads: Vec, + /// Where each group starts in `quads`. + pub offsets: [u32; GROUPS], +} + +impl BuiltSection { + pub fn empty(key: SectionKey, child_existence: u8) -> Self { + Self { + key, + child_existence, + aabb: 0, + quads: Vec::new(), + offsets: [0; GROUPS], + } + } +} + +/// The neighbouring sections' slices the outer passes read: -x, +x, -y, +y, +/// -z, +z, each 32x32. +const SIDES: [[i32; 3]; 6] = [ + [-1, 0, 0], + [1, 0, 0], + [0, -1, 0], + [0, 1, 0], + [0, 0, -1], + [0, 0, 1], +]; + +pub(crate) struct Factory { + /// The level of the section being meshed. + level: u8, + /// Per voxel, its quad without a position, then its model's metadata; + /// air keeps only its light. + section: Box<[u64]>, + /// Per voxel, [`voxel::gap`]. + gaps: Box<[u8]>, + /// The six neighbours' facing slices ([`SIDES`]). + neighbours: Box<[u64]>, + /// A bit per x of each row `y * 32 + z`: fully opaque blocks, blocks + /// neither opaque nor fluid, and fluids or blocks holding one. + opaque: Box<[u32; 1024]>, + non_opaque: Box<[u32; 1024]>, + fluid: Box<[u32; 1024]>, + /// The y and z passes' meshers: positive faces, then negative ones of + /// the blocks that aren't opaque. + mesher: Mesher, + secondary: Mesher, + /// The x pass's, one per layer, the same way. + x_meshers: Box<[Mesher; 32]>, + x_secondary: Box<[Mesher; 32]>, + out: Quads, +} + +impl Factory { + pub fn new() -> Self { + Self { + level: 0, + section: vec![0; VOLUME * 2].into(), + gaps: vec![0; VOLUME].into(), + neighbours: vec![UNSEEN; 6 * 1024].into(), + opaque: Box::new([0; 1024]), + non_opaque: Box::new([0; 1024]), + fluid: Box::new([0; 1024]), + mesher: Mesher::new(0, 0, true), + secondary: Mesher::new(0, 0, false), + x_meshers: Box::new(std::array::from_fn(|i| Mesher::new(2, i as u8, true))), + x_secondary: Box::new(std::array::from_fn(|i| Mesher::new(2, i as u8, false))), + out: Quads::new(), + } + } + + /// `section` at `key` meshed, `neighbour` giving the sections around it + /// (missing ones are unseen air). + pub fn build<'a>( + &mut self, + key: SectionKey, + section: &'a WorldSection, + neighbour: impl Fn(SectionKey) -> Option<&'a WorldSection>, + models: &mut dyn Models, + ) -> BuiltSection { + self.out.clear(); + self.level = key.level; + let needed = self.prepare(§ion.data, models); + for (side, offset) in SIDES.iter().enumerate() { + if needed & 1 << side == 0 { + continue; + } + let slice = &mut self.neighbours[side * 1024..][..1024]; + let next = SectionKey { + level: key.level, + x: key.x + offset[0], + y: key.y + offset[1], + z: key.z + offset[2], + }; + match neighbour(next) { + Some(next) => { + for (i, voxel) in slice.iter_mut().enumerate() { + *voxel = next.data[neighbour_index(side, i)]; + } + } + None => slice.fill(UNSEEN), + } + } + for axis in 0..2 { + self.mesher.axis = axis; + self.secondary.axis = axis; + self.opaque_inner(axis); + self.opaque_outer(axis, models); + self.fluid_inner(axis, models); + self.fluid_outer(axis, models); + self.non_opaque_inner(axis); + self.non_opaque_outer(axis, models); + } + self.x_opaque_inner(); + self.x_opaque_outer(models); + self.finish_x(); + self.x_fluid_inner(models); + self.x_fluid_outer(models); + self.finish_x(); + self.x_non_opaque_inner(); + self.x_non_opaque_outer(models); + self.finish_x(); + + if self.out.count() == 0 { + return BuiltSection::empty(key, section.non_empty_children); + } + let mut offsets = [0; GROUPS]; + let mut quads = Vec::with_capacity(self.out.count()); + for (group, offset) in self.out.groups.iter().zip(&mut offsets) { + *offset = quads.len() as u32; + quads.extend_from_slice(group); + } + let [min_x, min_y, min_z] = self.out.min.map(|c| c as u32); + let extent = |axis: usize| (self.out.max[axis] - self.out.min[axis] - 1).max(0) as u32; + let aabb = + min_x | min_y << 5 | min_z << 10 | extent(0) << 15 | extent(1) << 20 | extent(2) << 25; + BuiltSection { + key, + child_existence: section.non_empty_children, + aabb, + quads, + offsets, + } + } + + /// Each voxel's quad and metadata and the row masks; returns which sides + /// have blocks against them ([`SIDES`]). + fn prepare(&mut self, data: &[u64], models: &mut dyn Models) -> u8 { + let mut needed = 0; + for y in 0..32 { + for z in 0..32 { + let (mut not_empty, mut opaque, mut pure_fluid, mut partial_fluid) = + (0u32, 0, 0, 0); + for x in 0..32 { + let i = section::index(x, y, z); + let voxel = data[i]; + let model = if voxel::is_air(voxel) { + 0 + } else { + models.model(voxel::block_id(voxel)) + }; + let (quad, meta) = if model == 0 { + ((voxel & voxel::LIGHT_MASK) >> 1, Metadata(0)) + } else { + let meta = models.metadata(model); + not_empty |= 1 << x; + opaque |= u32::from(meta.is_fully_opaque()) << x; + pure_fluid |= u32::from(meta.is_fluid()) << x; + partial_fluid |= u32::from(meta.contains_fluid()) << x; + (pack_partial(model, voxel, meta), meta) + }; + self.section[i * 2] = quad; + self.section[i * 2 + 1] = meta.0; + self.gaps[i] = voxel::gap(voxel); + } + let row = y * 32 + z; + self.opaque[row] = opaque; + self.non_opaque[row] = (not_empty ^ opaque) & !pure_fluid; + self.fluid[row] = pure_fluid | partial_fluid; + if not_empty != 0 { + needed |= (not_empty & 1) as u8 + | ((not_empty >> 31) << 1) as u8 + | u8::from(y == 0) << 2 + | u8::from(y == 31) << 3 + | u8::from(z == 0) << 4 + | u8::from(z == 31) << 5; + } + } + } + needed + } + + fn quad(&self, i: usize) -> u64 { + self.section[i * 2] + } + + /// Voxel `i`'s quad and metadata as a fluid, at the voxel's surface; a + /// block holding one takes the fluid's model and group for the fluid + /// passes. + fn as_fluid(&self, models: &mut dyn Models, i: usize) -> (u64, Metadata) { + let (quad, meta) = (self.quad(i), self.meta(i)); + if !meta.has_fluid() { + return (quad, meta); + } + let model = ((quad & MODEL) >> QUAD_MODEL_SHIFT) as u16; + let fluid = fluid_of(models, model, self.level, self.gaps[i]); + let meta = models.metadata(fluid); + ( + quad & !MODEL & !0b110 | u64::from(fluid) << QUAD_MODEL_SHIFT | quad_group(meta), + meta, + ) + } + + fn meta(&self, i: usize) -> Metadata { + Metadata(self.section[i * 2 + 1]) + } + + fn neighbour(&self, side: usize, i: usize) -> u64 { + self.neighbours[side * 1024 + i] + } + + /// The neighbouring section's voxel against row `other`, x `index` of + /// `axis`'s outer layer on `side`. + fn across(&self, axis: u8, side: usize, other: usize, index: i32) -> u64 { + self.neighbour((axis as usize + 1) * 2 + side, other * 32 + index as usize) + } + + /// The neighbouring block as the fluid passes see it: a fluid's surface + /// model, with the block's own metadata. + fn fluid_neighbour(&self, models: &mut dyn Models, voxel: u64) -> (u16, Metadata) { + let (model, meta) = Self::neighbour_model(models, voxel); + if model != 0 && meta.has_fluid() { + (fluid_of(models, model, self.level, voxel::gap(voxel)), meta) + } else { + (model, meta) + } + } + + /// The neighbouring block's model and metadata, 0 for air. + fn neighbour_model(models: &mut dyn Models, voxel: u64) -> (u16, Metadata) { + if voxel::is_air(voxel) { + return (0, Metadata(0)); + } + let model = models.model(voxel::block_id(voxel)); + (model, models.metadata(model)) + } + + /// Voxel `index` of the y or z pass's row `pidx` in `axis`'s layer. + fn row_index(axis: u8, layer: usize, other: usize) -> usize { + if axis == 0 { + layer * 32 + other + } else { + other * 32 + layer + } + } + + /// The voxel `idx` a face belongs to and the one across it, `step` + /// voxels on: `idx` faces forward when it's the solid one. + fn pair(idx: usize, step: usize, forward: u32) -> (usize, usize) { + if forward == 1 { + (idx, idx + step) + } else { + (idx + step, idx) + } + } + + /// The y and z passes' walk of `axis`'s layer `layer`, row by row: + /// `mask` gives a row's faces by x, `each` meshes one, and the meshers + /// skip the rest (the secondary too when `both`). + fn walk( + &mut self, + axis: u8, + layer: usize, + both: bool, + mask: impl Fn(&Self, usize) -> u32, + mut each: impl FnMut(&mut Self, usize, usize, i32), + ) { + self.mesher.position = layer as u8; + self.secondary.position = layer as u8; + let mut skips = 0; + for other in 0..32 { + let pidx = Self::row_index(axis, layer, other); + let mask = mask(self, pidx); + if mask == 0 { + skips += 32; + continue; + } + self.mesher.skip(skips, &mut self.out); + if both { + self.secondary.skip(skips, &mut self.out); + } + skips = 0; + for (index, gap) in faces(mask) { + self.mesher.skip(gap, &mut self.out); + if both { + self.secondary.skip(gap, &mut self.out); + } + each(self, pidx, other, index); + } + self.mesher.end_row(&mut self.out); + if both { + self.secondary.end_row(&mut self.out); + } + } + self.mesher.finish(&mut self.out); + if both { + self.secondary.finish(&mut self.out); + } + } + + /// Mask rows between the layers along `axis`. + fn step(axis: u8) -> usize { + if axis == 0 { 32 } else { 1 } + } + + /// The faces between opaque blocks and everything else, between the + /// layers along `axis`. + fn opaque_inner(&mut self, axis: u8) { + self.mesher.face_offset = true; + let step = Self::step(axis); + for layer in 0..31 { + self.walk( + axis, + layer, + false, + |s, pidx| s.opaque[pidx] ^ s.opaque[pidx + step], + |s, pidx, _, index| { + let forward = s.opaque[pidx] >> index & 1; + let (own, next) = Self::pair(index as usize + pidx * 32, step * 32, forward); + let (own_quad, next_quad, next_meta) = + (s.quad(own), s.quad(next), s.meta(next)); + let culls = (own_quad ^ next_quad) & MODEL == 0 && next_meta.culls_same() + || next_meta.face_occludes(axis << 1 | (1 - forward) as u8); + let quad = (!culls).then(|| { + apply_light( + u64::from(forward) | own_quad & !QUAD_LIGHT | next_quad & QUAD_LIGHT, + s.meta(own), + ) + }); + put_or_skip(&mut s.mesher, quad, &mut s.out); + }, + ); + } + } + + /// The opaque faces on the section's two sides along `axis`, against the + /// neighbouring sections. + fn opaque_outer(&mut self, axis: u8, models: &mut dyn Models) { + self.mesher.face_offset = false; + for side in 0..2usize { + self.walk( + axis, + side * 31, + false, + |s, pidx| s.opaque[pidx], + |s, pidx, other, index| { + let idx = index as usize + pidx * 32; + let voxel = s.across(axis, side, other, index); + let own_quad = s.quad(idx); + let (model, meta) = Self::neighbour_model(models, voxel); + let culls = model != 0 + && (meta.is_fully_opaque() + || u64::from(model) << QUAD_MODEL_SHIFT == own_quad & MODEL + && meta.culls_same() + || meta.face_occludes(axis << 1 | (1 - side) as u8)); + let quad = (!culls).then(|| { + apply_light( + side as u64 | own_quad & !QUAD_LIGHT | neighbour_light(voxel), + s.meta(idx), + ) + }); + put_or_skip(&mut s.mesher, quad, &mut s.out); + }, + ); + } + self.mesher.face_offset = true; + } + + /// Fluid faces between layers, a block holding a fluid drawing the + /// fluid's model instead. + fn fluid_inner(&mut self, axis: u8, models: &mut dyn Models) { + let step = Self::step(axis); + for layer in 0..31 { + self.walk( + axis, + layer, + false, + |s, pidx| { + let (current, next) = (s.fluid[pidx], s.fluid[pidx + step]); + ((current | s.opaque[pidx]) ^ (next | s.opaque[pidx + step])) & (current | next) + }, + |s, pidx, _, index| { + let forward = s.fluid[pidx] >> index & 1; + let (own, next) = Self::pair(index as usize + pidx * 32, step * 32, forward); + let quad = + (!s.meta(next).face_occludes(axis << 1 | (1 - forward) as u8)).then(|| { + let (quad, meta) = s.as_fluid(models, own); + apply_light( + u64::from(forward) | quad & !QUAD_LIGHT | s.quad(next) & QUAD_LIGHT, + meta, + ) + }); + put_or_skip(&mut s.mesher, quad, &mut s.out); + }, + ); + } + } + + /// The fluid faces on the section's two sides along `axis`, against the + /// neighbouring sections; a neighbour holding a fluid culls as that fluid. + /// Fluids cull by kind rather than model, as their surfaces differ by + /// gap from section to section. + fn fluid_outer(&mut self, axis: u8, models: &mut dyn Models) { + self.mesher.face_offset = false; + for side in 0..2usize { + self.walk( + axis, + side * 31, + false, + |s, pidx| s.fluid[pidx], + |s, pidx, other, index| { + let idx = index as usize + pidx * 32; + let voxel = s.across(axis, side, other, index); + let (quad, meta) = s.as_fluid(models, idx); + let (model, next_meta) = s.fluid_neighbour(models, voxel); + let culls = outer_culls( + same_fluid(models, quad, model), + meta, + model, + next_meta, + axis << 1 | (1 - side) as u8, + ); + let quad = (!culls).then(|| { + apply_light( + side as u64 | quad & !QUAD_LIGHT | neighbour_light(voxel), + meta, + ) + }); + put_or_skip(&mut s.mesher, quad, &mut s.out); + }, + ); + } + self.mesher.face_offset = true; + } + + /// Both faces of every block that's neither opaque nor fluid, in the + /// inner layers. + fn non_opaque_inner(&mut self, axis: u8) { + let step = Self::step(axis) * 32; + for layer in 1..31 { + self.walk( + axis, + layer, + true, + |s, pidx| s.non_opaque[pidx], + |s, pidx, _, index| { + let idx = index as usize + pidx * 32; + let (quad, meta) = (s.quad(idx), s.meta(idx)); + let below = non_opaque_face( + axis << 1, + quad, + meta, + s.quad(idx - step), + s.meta(idx - step), + ); + let above = non_opaque_face( + axis << 1 | 1, + quad, + meta, + s.quad(idx + step), + s.meta(idx + step), + ); + put_or_skip(&mut s.secondary, below, &mut s.out); + put_or_skip(&mut s.mesher, above, &mut s.out); + }, + ); + } + } + + /// The same on the two outer layers, the outward face against the + /// neighbouring section. + fn non_opaque_outer(&mut self, axis: u8, models: &mut dyn Models) { + for side in 0..2usize { + let step = Self::step(axis) as isize * 32 * (1 - side as isize * 2); + self.walk( + axis, + side * 31, + true, + |s, pidx| s.non_opaque[pidx], + |s, pidx, other, index| { + let idx = index as usize + pidx * 32; + let voxel = s.across(axis, side, other, index); + let (quad, meta) = (s.quad(idx), s.meta(idx)); + // Outward, against the neighbouring section's block. + let (model, next_meta) = Self::neighbour_model(models, voxel); + let fail = outer_culls( + u64::from(model) << QUAD_MODEL_SHIFT == quad & MODEL, + meta, + model, + next_meta, + axis << 1 | (1 - side) as u8, + ); + // Inward, against the next layer in. + let inner = (idx as isize + step) as usize; + let (inner_quad, inner_meta) = (s.quad(inner), s.meta(inner)); + let fail_inner = inner_meta.culls_same() && inner_quad & MODEL == quad & MODEL + || inner_meta.face_occludes(axis << 1 | side as u8); + // These faces keep the block's own light, not a + // neighbour's. + let positive = meta.face_exists(axis << 1 | 1) + && ((side == 1 && !fail) || (side == 0 && !fail_inner)); + let negative = meta.face_exists(axis << 1) + && ((side == 0 && !fail) || (side == 1 && !fail_inner)); + put_or_skip( + &mut s.mesher, + positive.then(|| apply_light(1 | quad, meta)), + &mut s.out, + ); + put_or_skip( + &mut s.secondary, + negative.then(|| apply_light(quad, meta)), + &mut s.out, + ); + }, + ); + } + } + + fn finish_x(&mut self) { + for mesher in self.x_meshers.iter_mut().chain(self.x_secondary.iter_mut()) { + mesher.finish(&mut self.out); + } + } + + /// The x pass's walk: every row `y * 32 + z` once, a mesher per x layer + /// (the secondary too when `both`); `mask` gives a row's faces, `each` + /// meshes one. Each layer's pending skips are counted up and flushed to + /// its meshers before a face lands in it and at the end of every row. + fn walk_x( + &mut self, + both: bool, + mask: impl Fn(&Self, usize) -> u32, + mut each: impl FnMut(&mut Self, usize, usize), + ) { + for y in 0..32 { + let mut pending = [0u32; 32]; + for z in 0..32 { + let i = y * 32 + z; + let mask = mask(self, i); + add_skips(&mut pending, mask); + for (index, _) in faces(mask) { + let index = index as usize; + let skips = std::mem::take(&mut pending[index]); + self.x_meshers[index].skip(skips, &mut self.out); + if both { + self.x_secondary[index].skip(skips, &mut self.out); + } + each(self, i, index); + } + } + for (index, skips) in pending.into_iter().enumerate() { + self.x_meshers[index].skip(skips, &mut self.out); + if both { + self.x_secondary[index].skip(skips, &mut self.out); + } + } + } + } + + /// [`Self::opaque_inner`] along x. + fn x_opaque_inner(&mut self) { + self.walk_x( + false, + |s, i| { + let row = s.opaque[i]; + (row ^ row >> 1) & !(1 << 31) + }, + |s, i, index| { + let forward = s.opaque[i] >> index & 1; + let (own, next) = Self::pair(index + i * 32, 1, forward); + let quad = (!s.meta(next).face_occludes(2 << 1 | (1 - forward) as u8)).then(|| { + apply_light( + u64::from(forward) | s.quad(own) & !QUAD_LIGHT | s.quad(next) & QUAD_LIGHT, + s.meta(own), + ) + }); + put_or_skip(&mut s.x_meshers[index], quad, &mut s.out); + }, + ); + } + + /// [`Self::fluid_inner`] along x. + fn x_fluid_inner(&mut self, models: &mut dyn Models) { + self.walk_x( + false, + |s, i| { + let fluid = s.fluid[i]; + let row = s.opaque[i] | fluid; + (row ^ row >> 1) & !(1 << 31) & (fluid | fluid >> 1) + }, + |s, i, index| { + let forward = (s.opaque[i] | s.fluid[i]) >> index & 1; + let (own, next) = Self::pair(index + i * 32, 1, forward); + let quad = (!s.meta(next).face_occludes(2 << 1 | (1 - forward) as u8)).then(|| { + let (quad, meta) = s.as_fluid(models, own); + apply_light( + u64::from(forward) | quad & !QUAD_LIGHT | s.quad(next) & QUAD_LIGHT, + meta, + ) + }); + put_or_skip(&mut s.x_meshers[index], quad, &mut s.out); + }, + ); + } + + /// [`Self::non_opaque_inner`] along x. + fn x_non_opaque_inner(&mut self) { + self.walk_x( + true, + |s, i| s.non_opaque[i] & !0x8000_0001, + |s, i, index| { + let idx = index + i * 32; + let (quad, meta) = (s.quad(idx), s.meta(idx)); + let below = non_opaque_face(2 << 1, quad, meta, s.quad(idx - 1), s.meta(idx - 1)); + let above = + non_opaque_face(2 << 1 | 1, quad, meta, s.quad(idx + 1), s.meta(idx + 1)); + put_or_skip(&mut s.x_secondary[index], below, &mut s.out); + put_or_skip(&mut s.x_meshers[index], above, &mut s.out); + }, + ); + } + + /// The x outer passes' walk: at x 0 and 31 of every row `mask` marks, + /// `each` gives the quads for the layer's mesher and its secondary (or + /// none to skip), against the neighbouring section's voxel. The + /// layers' meshers start afresh, with `face_offset` for the pass. + fn walk_x_outer( + &mut self, + models: &mut dyn Models, + face_offset: bool, + mask: impl Fn(&Self, usize) -> u32, + mut each: impl FnMut(&Self, &mut dyn Models, usize, usize) -> [Option; 2], + ) { + for layer in [0, 31] { + self.x_meshers[layer].finish(&mut self.out); + self.x_secondary[layer].finish(&mut self.out); + self.x_meshers[layer].face_offset = face_offset; + } + for y in 0..32 { + let mut skips = [[0u32; 2]; 2]; + for z in 0..32 { + let i = y * 32 + z; + let row = mask(self, i); + for side in 0..2usize { + if row & 1 << (side * 31) == 0 { + skips[side] = skips[side].map(|n| n + 1); + continue; + } + let quads = each(self, models, side, i); + let layer = side * 31; + let [own_skips, secondary_skips] = &mut skips[side]; + for (mesher, quad, skips) in [ + (&mut self.x_meshers[layer], quads[0], own_skips), + (&mut self.x_secondary[layer], quads[1], secondary_skips), + ] { + match quad { + Some(quad) => { + mesher.skip(std::mem::take(skips), &mut self.out); + mesher.put(quad, &mut self.out); + } + None => *skips += 1, + } + } + } + } + for (side, [own_skips, secondary_skips]) in skips.into_iter().enumerate() { + let layer = side * 31; + self.x_meshers[layer].skip(own_skips, &mut self.out); + self.x_secondary[layer].skip(secondary_skips, &mut self.out); + } + } + for layer in [0, 31] { + self.x_meshers[layer].finish(&mut self.out); + self.x_secondary[layer].finish(&mut self.out); + self.x_meshers[layer].face_offset = true; + } + } + + /// [`Self::opaque_outer`] along x. + fn x_opaque_outer(&mut self, models: &mut dyn Models) { + self.walk_x_outer( + models, + false, + |s, i| s.opaque[i], + |s, models, side, i| { + let voxel = s.neighbour(side, i); + let (model, meta) = Self::neighbour_model(models, voxel); + let culls = model != 0 + && (meta.is_fully_opaque() || meta.face_occludes(2 << 1 | (1 - side) as u8)); + let idx = i * 32 + side * 31; + let quad = (!culls).then(|| { + apply_light( + side as u64 | s.quad(idx) & !QUAD_LIGHT | neighbour_light(voxel), + s.meta(idx), + ) + }); + [quad, None] + }, + ); + } + + /// [`Self::fluid_outer`] along x. + fn x_fluid_outer(&mut self, models: &mut dyn Models) { + self.walk_x_outer( + models, + false, + |s, i| s.fluid[i], + |s, models, side, i| { + let idx = i * 32 + side * 31; + let (quad, meta) = s.as_fluid(models, idx); + let voxel = s.neighbour(side, i); + let (model, next_meta) = s.fluid_neighbour(models, voxel); + let culls = outer_culls( + same_fluid(models, quad, model), + meta, + model, + next_meta, + 2 << 1 | (1 - side) as u8, + ); + let quad = (!culls).then(|| { + apply_light( + side as u64 | quad & !QUAD_LIGHT | neighbour_light(voxel), + meta, + ) + }); + [quad, None] + }, + ); + } + + /// At x 0 and 31, the outward face against the neighbouring section and + /// the inward one against the next block in. At x 0 the outward face is + /// -x, the secondary mesher's; at 31 it's +x, the layer's own. + fn x_non_opaque_outer(&mut self, models: &mut dyn Models) { + self.walk_x_outer( + models, + true, + |s, i| s.non_opaque[i], + |s, models, side, i| { + let idx = i * 32 + side * 31; + let (quad, meta) = (s.quad(idx), s.meta(idx)); + let voxel = s.neighbour(side, i); + let (model, next_meta) = Self::neighbour_model(models, voxel); + let out_face = (2 << 1) ^ side as u8; + let draw_out = if model == 0 { + meta.face_exists(out_face) + } else { + should_mesh_non_opaque( + out_face, + quad, + meta, + u64::from(model) << QUAD_MODEL_SHIFT, + next_meta, + ) + }; + let light = if meta.face_uses_self_lighting(out_face) { + quad & QUAD_LIGHT + } else { + neighbour_light(voxel) + }; + let outward = + draw_out.then(|| apply_light(side as u64 | quad & !QUAD_LIGHT | light, meta)); + let in_idx = if side == 0 { idx + 1 } else { idx - 1 }; + let inward = non_opaque_face( + (2 << 1 | 1) ^ side as u8, + quad, + meta, + s.quad(in_idx), + s.meta(in_idx), + ); + if side == 0 { + [inward, outward] + } else { + [outward, inward] + } + }, + ); + } +} + +/// The set bits of a row's `mask`: each face's x and the gap of nothing +/// since the one before. +fn faces(mask: u32) -> impl Iterator { + let mut rest = mask; + let mut last = -1; + std::iter::from_fn(move || { + if rest == 0 { + return None; + } + let index = rest.trailing_zeros() as i32; + rest &= rest - 1; + let gap = (index - last - 1) as u32; + last = index; + Some((index, gap)) + }) +} + +/// One more face of nothing for every x layer `faces` has no face in. +fn add_skips(pending: &mut [u32; 32], faces: u32) { + for (index, skips) in pending.iter_mut().enumerate() { + *skips += u32::from(faces & 1 << index == 0); + } +} + +/// The index in neighbour `side`'s data of the voxel facing this section at +/// slice position `i`. +fn neighbour_index(side: usize, i: usize) -> usize { + let (a, b) = (i & 31, i >> 5); + match side { + 0 => section::index(31, b, a), + 1 => section::index(0, b, a), + 2 => section::index(a, 31, b), + 3 => section::index(a, 0, b), + 4 => section::index(a, b, 31), + _ => section::index(a, b, 0), + } +} + +/// The voxel's biome and light moved down a bit into their quad fields, the +/// biome kept only for biome-coloured models, the light dropped for fully +/// opaque ones (they take the neighbour's); then the model and its group. +fn pack_partial(model: u16, voxel: u64, meta: Metadata) -> u64 { + let mut light_biome = (voxel & (voxel::BIOME_MASK | voxel::LIGHT_MASK)) >> 1; + if !meta.is_biome_coloured() { + light_biome &= !(0x1FF << QUAD_BIOME_SHIFT); + } + if meta.is_fully_opaque() { + light_biome &= !QUAD_LIGHT; + } + light_biome | u64::from(model) << QUAD_MODEL_SHIFT | quad_group(meta) +} + +/// The group in bits 1-2: translucent 0, double-sided 1, else 2 for the +/// face's own. +fn quad_group(meta: Metadata) -> u64 { + 0b111 + & (0b000_000_010_100 + >> (u64::from(meta.is_translucent()) * 6 + u64::from(meta.is_double_sided()) * 3)) +} + +/// A neighbouring section's voxel's light, in a quad's field. +fn neighbour_light(voxel: u64) -> u64 { + (voxel & voxel::LIGHT_MASK) >> 1 +} + +/// The block light at least the model's emission. +fn apply_light(quad: u64, meta: Metadata) -> u64 { + let light = (quad & BLOCK_LIGHT).max(meta.light_emission() << (QUAD_LIGHT_SHIFT + 4)); + quad & !BLOCK_LIGHT | light +} + +/// The model of the fluid `model`'s block holds, with its surface `gap` +/// blocks down into a level-`level` voxel. +fn fluid_of(models: &mut dyn Models, model: u16, level: u8, gap: u8) -> u16 { + let fluid = models.fluid_model(model); + models.fluid_surface(fluid, level, gap) +} + +/// Whether a fluid quad's surface and the neighbouring surface `next` are +/// the same fluid. +fn same_fluid(models: &dyn Models, quad: u64, next: u16) -> bool { + let own = ((quad & MODEL) >> QUAD_MODEL_SHIFT) as u16; + models.fluid_model(own) == models.fluid_model(next) +} + +/// Whether the neighbouring section's block, model `next`, hides a face of +/// a block with `meta`: not when it's air, else when it's the `same` kind +/// and the block culls its own, or its `face` against this one occludes. +fn outer_culls(same: bool, meta: Metadata, next: u16, next_meta: Metadata, face: u8) -> bool { + next != 0 && (same && meta.culls_same() || next_meta.face_occludes(face)) +} + +/// Whether `face` of a block that's neither opaque nor fluid is drawn against +/// `next`: not against its own kind when the model culls same, not when the +/// model has no such face, and not when the face can be occluded and `next` +/// occludes it. +fn should_mesh_non_opaque( + face: u8, + quad: u64, + meta: Metadata, + next_quad: u64, + next_meta: Metadata, +) -> bool { + if (quad ^ next_quad) & MODEL == 0 && meta.culls_same() { + return false; + } + if !meta.face_exists(face) { + return false; + } + !(meta.face_can_be_occluded(face) && next_meta.face_occludes(face ^ 1)) +} + +/// The quad to draw for `face` against `next`, if any. +fn non_opaque_face( + face: u8, + quad: u64, + meta: Metadata, + next_quad: u64, + next_meta: Metadata, +) -> Option { + should_mesh_non_opaque(face, quad, meta, next_quad, next_meta).then(|| { + let light = if meta.face_uses_self_lighting(face) { + quad + } else { + next_quad + } & QUAD_LIGHT; + apply_light(u64::from(face & 1) | quad & !QUAD_LIGHT | light, meta) + }) +} + +fn put_or_skip(mesher: &mut Mesher, quad: Option, out: &mut Quads) { + match quad { + Some(quad) => mesher.put(quad, out), + None => mesher.skip(1, out), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Models: 1 stone (opaque), 2 glass (sees through, culls its own + /// kind), 3 water (translucent biome-coloured fluid), 4 a waterlogged + /// slab-like block (holds water, so keeps the biome; open on top), 5 a + /// snow layer (a full occluder below, thin faces that can be occluded + /// elsewhere). + struct Stub; + + const OPAQUE_FACE: u64 = 0b0111; + const CLEAR_FACE: u64 = 0b1110; + + fn every_face(byte: u64) -> u64 { + (0..6).fold(0, |m, f| m | byte << (8 * f)) + } + + impl Models for Stub { + fn model(&mut self, block: u32) -> u16 { + block as u16 + } + + fn metadata(&self, model: u16) -> Metadata { + Metadata(match model { + 1 => every_face(OPAQUE_FACE) | 1 << 54, + 2 => every_face(CLEAR_FACE) | 32 << 48, + 3 => every_face(CLEAR_FACE) | (1 | 2 | 16 | 32) << 48, + 4 => every_face(OPAQUE_FACE) & !(0xFF << 8) | 0xFF << 8 | (1 | 8) << 48, + 5 => every_face(0b0100) & !0xFF | OPAQUE_FACE, + _ => 0, + }) + } + + fn fluid_model(&self, model: u16) -> u16 { + if model == 4 { 3 } else { model } + } + + fn fluid_surface(&mut self, fluid: u16, _level: u8, _gap: u8) -> u16 { + fluid + } + } + + fn key() -> SectionKey { + SectionKey { + level: 0, + x: 0, + y: 0, + z: 0, + } + } + + fn build(section: &WorldSection) -> BuiltSection { + Factory::new().build(key(), section, |_| None, &mut Stub) + } + + fn with(blocks: &[((usize, usize, usize), u32)]) -> WorldSection { + let mut section = WorldSection::unseen(); + for &((x, y, z), block) in blocks { + section.data[section::index(x, y, z)] = voxel::compose(0x0F, block, 0); + } + section + } + + fn face(quad: u64) -> u8 { + (quad & 7) as u8 + } + + fn position(quad: u64) -> (u32, u32, u32) { + ( + (quad >> 21 & 31) as u32, + (quad >> 16 & 31) as u32, + (quad >> 11 & 31) as u32, + ) + } + + fn model(quad: u64) -> u16 { + ((quad & MODEL) >> QUAD_MODEL_SHIFT) as u16 + } + + #[test] + fn a_snow_layer_on_a_block_draws_its_top() { + let built = build(&with(&[((5, 6, 7), 1), ((5, 7, 7), 5)])); + let described: Vec<(u8, (u32, u32, u32), u16)> = built + .quads + .iter() + .map(|&q| (face(q), position(q), model(q))) + .collect(); + assert!( + described.contains(&(1, (5, 7, 7), 5)), + "no snow top in {described:?}" + ); + assert!(!described.contains(&(1, (5, 6, 7), 1)), "{described:?}"); + } + + #[test] + fn a_lone_cube_draws_its_six_faces_where_it_stands() { + let built = build(&with(&[((5, 6, 7), 1)])); + assert_eq!(built.quads.len(), 6); + let mut seen: Vec = built.quads.iter().map(|&q| face(q)).collect(); + seen.sort_unstable(); + assert_eq!(seen, [0, 1, 2, 3, 4, 5]); + for &quad in &built.quads { + assert_eq!(position(quad), (5, 6, 7)); + assert_eq!(model(quad), 1); + assert_eq!(quad >> 3 & 0xFF, 0, "one block wide and high"); + assert_eq!( + quad >> QUAD_LIGHT_SHIFT & 0xFF, + 0x0F, + "lit by the air around" + ); + } + // Each face in its own group, after the empty translucent and + // double-sided ones. + assert_eq!(built.offsets, [0, 0, 0, 1, 2, 3, 4, 5]); + assert_eq!(built.aabb & 0x7FFF, 5 | 6 << 5 | 7 << 10); + } + + #[test] + fn touching_cubes_hide_their_shared_faces_and_merge() { + let pair = build(&with(&[((5, 6, 7), 1), ((6, 6, 7), 1)])); + // Their faces merge into one quad per side. + assert_eq!(pair.quads.len(), 6); + let tops: Vec = pair + .quads + .iter() + .copied() + .filter(|&q| face(q) == 1) + .collect(); + assert_eq!(tops.len(), 1); + assert_eq!(tops[0] >> 3 & 0xF, 1, "two wide"); + // A full section shows only its outside, each side in 16x16 quads. + let mut full = WorldSection::unseen(); + full.data.fill(voxel::compose(0, 1, 0)); + assert_eq!(build(&full).quads.len(), 6 * 4); + } + + #[test] + fn glass_culls_against_glass_but_not_stone() { + let glass = build(&with(&[((5, 6, 7), 2), ((6, 6, 7), 2)])); + assert_eq!(glass.quads.len(), 6); + // Beside stone, glass keeps its face (stone's hides behind glass). + let mixed = build(&with(&[((5, 6, 7), 2), ((6, 6, 7), 1)])); + assert_eq!(mixed.quads.iter().filter(|&&q| model(q) == 2).count(), 5); + assert_eq!(mixed.quads.iter().filter(|&&q| model(q) == 1).count(), 6); + } + + /// A [`Stub`] that notes the surfaces asked for. + struct Surfaces(Vec<(u16, u8, u8)>); + + impl Models for Surfaces { + fn model(&mut self, block: u32) -> u16 { + Stub.model(block) + } + + fn metadata(&self, model: u16) -> Metadata { + Stub.metadata(model) + } + + fn fluid_model(&self, model: u16) -> u16 { + Stub.fluid_model(model) + } + + fn fluid_surface(&mut self, fluid: u16, level: u8, gap: u8) -> u16 { + self.0.push((fluid, level, gap)); + fluid + } + } + + /// Face 1's quads of `built` as the columns they cover, counted, with + /// the quad's two extents taken along x then z, or the other way round. + fn top_coverage(built: &BuiltSection, swapped: bool) -> Vec { + let mut cover = vec![0u32; 1024]; + for &quad in built.quads.iter().filter(|&&q| face(q) == 1) { + let (x, _, z) = position(quad); + let (mut a, mut b) = ((quad >> 3 & 15) as u32 + 1, (quad >> 7 & 15) as u32 + 1); + if swapped { + std::mem::swap(&mut a, &mut b); + } + for dx in 0..a { + for dz in 0..b { + cover[((z + dz) * 32 + x + dx) as usize] += 1; + } + } + } + cover + } + + #[test] + fn a_lake_surface_covers_every_column_once() { + lake_surface_covers_every_column_once(11); + } + + #[test] + fn a_lake_surface_at_the_sections_top_covers_every_column_once() { + lake_surface_covers_every_column_once(32); + } + + fn lake_surface_covers_every_column_once(depth: usize) { + let mut lake = WorldSection::unseen(); + for x in 0..32 { + for z in 0..32 { + for y in 0..depth { + lake.data[section::index(x, y, z)] = voxel::compose(0x0F, 3, 0); + } + } + } + let built = build(&lake); + let holes = |swapped| { + top_coverage(&built, swapped) + .iter() + .filter(|&&c| c != 1) + .count() + }; + assert!( + holes(false) == 0 || holes(true) == 0, + "{} / {} columns not covered exactly once", + holes(false), + holes(true) + ); + } + + #[test] + fn a_coarse_fluid_meshes_at_its_surface() { + let mut lake = WorldSection::unseen(); + for x in 0..32 { + for z in 0..32 { + lake.data[section::index(x, 10, z)] = voxel::compose(0x0F, 3, 0) | 1; + } + } + let mut surfaces = Surfaces(Vec::new()); + let level_1 = SectionKey { level: 1, ..key() }; + Factory::new().build(level_1, &lake, |_| None, &mut surfaces); + assert!(surfaces.0.contains(&(3, 1, 1))); + assert!(surfaces.0.iter().all(|&surface| surface == (3, 1, 1))); + } + + #[test] + fn a_waterlogged_block_draws_its_water_around_it() { + let built = build(&with(&[((5, 6, 7), 4)])); + // The water fills the block, so it shows on every open side; the + // block itself has no top face. + let water: Vec = built + .quads + .iter() + .copied() + .filter(|&q| model(q) == 3) + .collect(); + assert_eq!(water.len(), 6); + assert_eq!( + built.offsets[1], 6, + "the water quads are the translucent group" + ); + let block: Vec = built + .quads + .iter() + .copied() + .filter(|&q| model(q) == 4) + .collect(); + assert_eq!(block.len(), 5); + assert!(block.iter().all(|&q| face(q) != 1)); + } + + #[test] + fn section_borders_face_the_neighbouring_section() { + let mut stone = WorldSection::unseen(); + stone.data.fill(voxel::compose(0, 1, 0)); + let edge = with(&[((0, 6, 7), 1)]); + // A stone neighbour on -x hides the face against it. + let hidden = + Factory::new().build(key(), &edge, |k| (k.x == -1).then_some(&stone), &mut Stub); + assert_eq!(hidden.quads.len(), 5); + assert!(hidden.quads.iter().all(|&q| face(q) != 4)); + // Nothing there: drawn, lit by the unseen sky. + let open = build(&edge); + let west = open.quads.iter().find(|&&q| face(q) == 4).unwrap(); + assert_eq!(west >> QUAD_LIGHT_SHIFT & 0xFF, 0x0F); + } + + #[test] + fn section_borders_cull_along_y_and_z_too() { + let mut stone = WorldSection::unseen(); + stone.data.fill(voxel::compose(0, 1, 0)); + let on_stone = with(&[((5, 0, 7), 1)]); + let built = Factory::new().build( + key(), + &on_stone, + |k| (k.y == -1).then_some(&stone), + &mut Stub, + ); + assert_eq!(built.quads.len(), 5); + assert!(built.quads.iter().all(|&q| face(q) != 0)); + // Glass against glass in the section to the -z hides that face. + let glass = with(&[((5, 6, 31), 2)]); + let edge = with(&[((5, 6, 0), 2)]); + let built = + Factory::new().build(key(), &edge, |k| (k.z == -1).then_some(&glass), &mut Stub); + assert_eq!(built.quads.len(), 5); + assert!(built.quads.iter().all(|&q| face(q) != 2)); + } + + #[test] + fn water_in_a_waterlogged_block_keeps_its_biome() { + let mut section = WorldSection::unseen(); + section.data[section::index(5, 6, 7)] = voxel::compose(0x0F, 4, 7); + let water: Vec = build(§ion) + .quads + .into_iter() + .filter(|&q| model(q) == 3) + .collect(); + assert_eq!(water.len(), 6); + assert!(water.iter().all(|&q| q >> QUAD_BIOME_SHIFT & 0x1FF == 7)); + } + + /// A [`Stub`] whose coarse water surfaces are models of their own, 100 + /// plus the gap, as [`super::super::models::ModelFactory`]'s are. + struct Gapped; + + impl Models for Gapped { + fn model(&mut self, block: u32) -> u16 { + Stub.model(block) + } + + fn metadata(&self, model: u16) -> Metadata { + Stub.metadata(if model >= 100 { 3 } else { model }) + } + + fn fluid_model(&self, model: u16) -> u16 { + if model >= 100 { + 3 + } else { + Stub.fluid_model(model) + } + } + + fn fluid_surface(&mut self, fluid: u16, level: u8, gap: u8) -> u16 { + if level == 0 { + fluid + } else { + 100 + u16::from(gap) + } + } + } + + #[test] + fn coarse_water_hides_its_faces_against_water_of_another_surface() { + let level_1 = |y| SectionKey { + level: 1, + y, + ..key() + }; + let water = |gap| voxel::compose(0x0F, 3, 0) | gap; + let mut below = WorldSection::unseen(); + below.data[section::index(5, 31, 7)] = water(0); + let mut above = WorldSection::unseen(); + above.data[section::index(5, 0, 7)] = water(1); + let built = Factory::new().build( + level_1(1), + &above, + |k| (k.y == 0).then_some(&below), + &mut Gapped, + ); + assert_eq!(built.quads.len(), 5); + assert!(built.quads.iter().all(|&q| face(q) != 0 && model(q) == 101)); + let built = Factory::new().build( + level_1(0), + &below, + |k| (k.y == 1).then_some(&above), + &mut Gapped, + ); + assert_eq!(built.quads.len(), 5); + assert!(built.quads.iter().all(|&q| face(q) != 1)); + // Along x the same, between columns at different surfaces. + let mut west = WorldSection::unseen(); + west.data[section::index(31, 5, 7)] = water(3); + let mut east = WorldSection::unseen(); + east.data[section::index(0, 5, 7)] = water(2); + let built = Factory::new().build( + level_1(0), + &east, + |k| (k.x == -1).then_some(&west), + &mut Gapped, + ); + assert_eq!(built.quads.len(), 5); + assert!(built.quads.iter().all(|&q| face(q) != 4)); + } + + #[test] + fn empty_sections_build_nothing() { + let mut empty = WorldSection::unseen(); + empty.non_empty_children = 0b101; + let built = build(&empty); + assert!(built.quads.is_empty()); + assert_eq!(built.child_existence, 0b101); + } +} diff --git a/pomme-client/src/renderer/distant/colors.rs b/pomme-client/src/renderer/distant/colors.rs new file mode 100644 index 000000000..045012045 --- /dev/null +++ b/pomme-client/src/renderer/distant/colors.rs @@ -0,0 +1,121 @@ +//! What distant terrain's models bake from, and the biome tints the shaders +//! look them up under. + +use std::collections::HashMap; +use std::sync::Arc; + +use azalea_block::BlockState; + +use super::gpu::TINT_BIOMES; +use super::models::{Metadata, ModelFactory, Models, pack_tint}; +use crate::renderer::chunk::atlas::AtlasUVMap; +use crate::renderer::chunk::mesher::{ + BiomeClimate, Colormap, dry_foliage_color, foliage_color, grass_color, +}; +use crate::world::block::BlockStateTable; +use crate::world::block::render_assets::OwnedBlockRenderView; + +/// Biome tint kinds in the tint buffer: grass, foliage, dry foliage, water +/// ([`super::models::ModelTint`]'s biome kinds, in order). +pub(crate) const TINT_KINDS: usize = 4; + +pub(crate) struct DistantColors { + render_view: OwnedBlockRenderView, + uv_map: Arc, + climates: Arc>, + grass: Arc, + foliage: Arc, + dry_foliage: Arc, +} + +impl DistantColors { + pub fn new( + render_view: OwnedBlockRenderView, + uv_map: Arc, + climates: Arc>, + (grass, foliage, dry_foliage): (Arc, Arc, Arc), + ) -> Self { + Self { + render_view, + uv_map, + climates, + grass, + foliage, + dry_foliage, + } + } + + pub fn table(&self) -> BlockStateTable { + self.render_view.block_state_table() + } + + /// Whether `models` were baked from the same textures and state ids as + /// these would bake them. + pub fn same_models(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.uv_map, &other.uv_map) + && self.table().table_slot() == other.table().table_slot() + } + + /// The models as the mesher asks for them, baked into `models` the + /// first time. + pub fn baking<'a>(&'a self, models: &'a mut ModelFactory) -> Baking<'a> { + Baking { + colors: self, + models, + } + } + + /// The tint buffer, [`TINT_KINDS`] rows of [`TINT_BIOMES`]: each biome's + /// grass, foliage, dry foliage and water colours; white for biomes + /// unknown. Without a position, the biome's base colour (no swamp or + /// dark forest noise). + pub fn tint_lut(&self) -> Vec { + let mut lut = vec![u32::MAX; TINT_KINDS * TINT_BIOMES]; + for (&biome, climate) in self.climates.iter() { + if biome >= super::voxel::UNKNOWN_BIOME { + continue; + } + let biome = biome as usize; + let colours = [ + grass_color(climate, &self.grass, 0, 0), + foliage_color(climate, &self.foliage), + dry_foliage_color(climate, &self.dry_foliage), + climate.water_color, + ]; + for (kind, rgb) in colours.into_iter().enumerate() { + lut[kind * TINT_BIOMES + biome] = pack_tint(rgb); + } + } + lut + } +} + +/// Block states' models for the mesher. +pub(crate) struct Baking<'a> { + colors: &'a DistantColors, + models: &'a mut ModelFactory, +} + +impl Models for Baking<'_> { + fn model(&mut self, block: u32) -> u16 { + BlockState::try_from(block).map_or(0, |state| { + self.models.model( + self.colors.render_view.as_view(), + &self.colors.uv_map, + state, + ) + }) + } + + fn metadata(&self, model: u16) -> Metadata { + self.models.info(model).metadata + } + + fn fluid_surface(&mut self, fluid: u16, level: u8, gap: u8) -> u16 { + self.models.fluid_surface(fluid, level, gap) + } + + fn fluid_model(&self, model: u16) -> u16 { + self.models.info(model).fluid + } +} diff --git a/pomme-client/src/renderer/distant/gpu.rs b/pomme-client/src/renderer/distant/gpu.rs new file mode 100644 index 000000000..82faaeac8 --- /dev/null +++ b/pomme-client/src/renderer/distant/gpu.rs @@ -0,0 +1,1701 @@ +//! Distant terrain on the GPU: built sections' quads in one arena with a +//! metadata record each, the models they index, the node tree the GPU walks +//! each frame to pick which to draw, the requests it sends back, and the +//! draw list the GPU builds from that pick. + +use std::collections::{BTreeMap, HashMap, HashSet, VecDeque}; +use std::sync::Arc; + +use glam::DVec3; + +use super::build::BuiltSection; +use super::colors::TINT_KINDS; +use super::models::{ + ATLAS_COLUMNS, ATLAS_ROWS, BlockModel, CAPACITY, MODEL_MIP_LEVELS, ModelTile, TILE_HEIGHT, + TILE_WIDTH, +}; +use super::nodes::{NODE_BYTES, NodeGraph}; +use super::section::{LEVELS, SIDE, SectionKey}; +use super::worker::DistantUpdate; +use crate::position::ChunkPos; +use crate::renderer::chunk::buffer::DrawCommand; +use crate::renderer::chunk::tlsf::Tlsf; +use crate::renderer::rhi::IndirectDrawFeatures; +use crate::renderer::{MAX_FRAMES_IN_FLIGHT, rhi, shader, util}; + +/// The sizes and markers the shaders share. +pub(crate) mod consts { + include!("shader_consts.rs"); +} + +use consts::{ + CLEANER_OUTPUT, CLEANER_SENTINEL, CLEANER_SORT_ELEMS, CLEANER_SORT_THREADS, MAX_NODES, + NODE_SCATTER_BIT, OPAQUE_DRAWS, QUADS_PER_DRAW, SEEN_BEFORE_BIT, TEMPORAL_DRAWS, + TEMPORAL_OFFSET, TRANSLUCENT_DRAWS, TRANSLUCENT_OFFSET, +}; +pub(crate) use consts::{ + COUNTS_CULL_DRAW, COUNTS_OPAQUE, COUNTS_TEMPORAL, COUNTS_TRANSLUCENT, LIST_GROUP, MAX_LISTED, + MAX_REQUESTS, TRANSLUCENT_BUCKETS, +}; + +const _: () = assert!(consts::LEVELS == LEVELS as u32); +/// The traversal's workgroup. +pub(crate) const TRAVERSAL_GROUP: u32 = 1 << consts::TRAVERSAL_GROUP_BITS; + +/// The arena's size: an eighth of the device's memory, within these bounds. +const MAX_ARENA_BYTES: u64 = 512 << 20; +const MIN_ARENA_BYTES: u64 = 64 << 20; +/// Sections the metadata buffer holds. +const MAX_SECTIONS: u32 = 1 << 20; +/// Quads a frame's staging holds for the arena. +const STAGING_BYTES: u64 = 16 << 20; +/// Records a frame's scatter list holds (`distant_scatter.comp`: chunks of +/// four indices and four records). +const SCATTER_RECORDS: usize = 16384; +const SCATTER_BYTES: u64 = (SCATTER_RECORDS / 4 * 80) as u64; +const MODELS_BYTES: u64 = (CAPACITY * size_of::()) as u64; +const TINTS_BYTES: u64 = (TINT_BIOMES * TINT_KINDS * size_of::()) as u64; +/// A model tile's bytes, every level. +const TILE_BYTES: usize = { + let mut bytes = 0; + let mut level = 0; + while level < MODEL_MIP_LEVELS { + bytes += ((TILE_WIDTH >> level) * (TILE_HEIGHT >> level) * 4) as usize; + level += 1; + } + bytes +}; +/// Model tiles uploaded a frame; more wait for the next. +const TILES_PER_FRAME: usize = 256; +/// Biome tints per kind in the colour buffer ([`super::colors`]). +pub(crate) const TINT_BIOMES: usize = 512; +/// The requests in flight at which the traversal stops asking for more. +const TARGET_REQUESTS: f64 = 4000.0; +/// Top-level nodes the ring may hold. +const MAX_ROOTS: usize = 1 << 14; +/// The distant projection's far plane: 3000 chunks. +pub(crate) const DISTANT_FAR: f32 = 3000.0 * 16.0; +/// Drawn chunk sections the bound store starts with room for. +const BOUND_CAPACITY: u32 = 1 << 15; +/// The box's 12 triangles as corner bits (x, y, z), every face wound +/// counter-clockwise seen from outside, so culling the back faces keeps +/// the near ones and culling the front faces the far ones; after the +/// quads' indices in the shared index buffer. +const CUBE: [u32; 36] = [ + 0, 6, 2, 6, 0, 4, 1, 7, 5, 7, 1, 3, 0, 5, 4, 5, 0, 1, 2, 7, 3, 7, 2, 6, 0, 3, 1, 3, 0, 2, 4, 7, + 6, 7, 4, 5, +]; +pub(crate) const CUBE_FIRST_INDEX: u32 = QUADS_PER_DRAW * 6; + +/// Byte sizes of the buffers the GPU passes bind. +pub(crate) const SECTIONS_BYTES: u64 = MAX_SECTIONS as u64 * 32; +pub(crate) const RENDER_LIST_BYTES: u64 = (1 + MAX_LISTED as u64) * 4; +pub(crate) const COUNTS_BYTES: u64 = 64; +const COUNT_WORDS: usize = COUNTS_BYTES as usize / 4; +pub(crate) const DRAWS_BYTES: u64 = + (TEMPORAL_OFFSET + TEMPORAL_DRAWS) as u64 * size_of::() as u64; +pub(crate) const POSITIONS_BYTES: u64 = MAX_LISTED as u64 * 8; +pub(crate) const VISIBILITY_BYTES: u64 = MAX_SECTIONS as u64 * 4; +pub(crate) const TRANSLUCENT_BYTES: u64 = (TRANSLUCENT_BUCKETS + TRANSLUCENT_DRAWS) as u64 * 4; +/// The request queue: a `uvec2` count, then the keys. +pub(crate) const REQUESTS_BYTES: u64 = 8 + MAX_REQUESTS as u64 * 8; +/// Per traversal iteration, a `uvec4`: its dispatch size and node count. +pub(crate) const QUEUE_META_BYTES: u64 = LEVELS as u64 * 16; +pub(crate) const SCRATCH_BYTES: u64 = MAX_LISTED as u64 * 4; +pub(crate) const TOP_IDS_BYTES: u64 = MAX_ROOTS as u64 * 4; +/// Per node, the frame the traversal last drew it, which the cleaner sorts by. +pub(crate) const NODE_VISIBILITY_BYTES: u64 = MAX_NODES as u64 * 4; +/// The cleaner's output: the ids it picks, then their keys. +pub(crate) const CLEANER_IDS_BYTES: u64 = CLEANER_OUTPUT as u64 * 4; +/// Per pick its key and its id (a `uvec4`), so a pick whose id now holds +/// another node is dropped (a departure: the reference reads back keys +/// alone, which after a dimension change can name a new node at the same +/// coordinates). +pub(crate) const CLEANER_KEYS_BYTES: u64 = CLEANER_OUTPUT as u64 * 16; +/// Nodes a workgroup of the cleaner's sort covers. +const CLEANER_SORT_GROUP: u32 = CLEANER_SORT_THREADS * CLEANER_SORT_ELEMS; +/// Visibility sets a frame's list holds. +const MAX_VISIBILITY_SETS: usize = 1 << 16; +pub(crate) const VISIBILITY_SETS_BYTES: u64 = MAX_VISIBILITY_SETS as u64 * 4; + +/// A section's quads in the arena and its record (`SectionMeta`). +#[derive(Clone, Copy)] +struct Uploaded { + /// Its record's slot. + id: u32, + /// Its first quad. + ptr: u32, +} + +/// A section placed in the arena, waiting for a frame's staging to carry +/// its quads and record up. +struct Pending { + uploaded: Uploaded, + quads: Vec, + record: [[u32; 4]; 2], +} + +/// The draw list's regions. +#[derive(Clone, Copy)] +pub(crate) enum Region { + Opaque, + /// Sections the occlusion pass saw this frame but not the frame before, + /// which the opaque region (built last frame) missed. + Temporal, + Translucent, +} + +impl Region { + const ALL: [Self; 3] = [Self::Opaque, Self::Temporal, Self::Translucent]; + + /// The region's first draw, the byte offset of its count in the + /// draw-count buffer, and its size in draws. + fn layout(self) -> (u32, u64, u32) { + match self { + Self::Opaque => (0, COUNTS_OPAQUE, OPAQUE_DRAWS), + Self::Temporal => (TEMPORAL_OFFSET, COUNTS_TEMPORAL, TEMPORAL_DRAWS), + Self::Translucent => (TRANSLUCENT_OFFSET, COUNTS_TRANSLUCENT, TRANSLUCENT_DRAWS), + } + } +} + +/// Where the player is and how far distant terrain draws. +#[derive(Clone, PartialEq)] +pub(crate) struct View { + pub dimension: Arc, + /// The dimension's block range, so the tree spans its height. + pub min_y: i32, + pub height: i32, + /// In blocks. + pub radius: f64, +} + +/// The buffers the GPU passes read and write. +#[derive(Clone, Copy)] +pub(crate) struct DrawBuffers<'a> { + pub sections: &'a rhi::backend::Buffer, + pub counts: &'a rhi::backend::Buffer, + pub draws: &'a rhi::backend::Buffer, + pub positions: &'a rhi::backend::Buffer, + pub visibility: &'a rhi::backend::Buffer, + pub translucent: &'a rhi::backend::Buffer, + pub render_list: &'a rhi::backend::Buffer, + pub nodes: &'a rhi::backend::Buffer, + pub requests: &'a rhi::backend::Buffer, + pub queue_meta: &'a rhi::backend::Buffer, + pub scratch: [&'a rhi::backend::Buffer; 2], + pub node_visibility: &'a rhi::backend::Buffer, + pub cleaner_ids: &'a rhi::backend::Buffer, + pub cleaner_keys: &'a rhi::backend::Buffer, +} + +pub(crate) struct DistantBuffers { + layout: rhi::backend::BindGroupLayout, + /// Set 1: the quads, models, tints and the model atlas. + group: rhi::backend::BindGroup, + /// Every section's quads. + arena: rhi::backend::Buffer, + quads: Tlsf, + /// Every section's record, by id. + sections: rhi::backend::Buffer, + ids: Tlsf, + /// Every uploaded section, by id. + meshes: HashMap, + index_buffer: rhi::backend::Buffer, + pending: VecDeque, + /// Results held until the arena has room for their quads. + waiting: HashMap, + graph: NodeGraph, + /// Per frame in flight: the arena's staging, the scatter list, the + /// top-level node ids, the queue metadata's staging, the visibility + /// sets, and the request queue's and the cleaner's readbacks. + quad_staging: Vec, + scatter_staging: Vec, + scatter: util::ComputePass, + top_ids: Vec, + queue_meta_staging: Vec, + visibility_sets: Vec, + request_readbacks: Vec, + cleaner_readbacks: Vec, + /// Without an indirect count: the draw counts read back per frame, + /// the last ones read, and the slots each region draws this frame. + count_readbacks: Vec, + last_counts: [u32; COUNT_WORDS], + slots: [u32; 3], + /// Frames recorded, which a frame ended at acquire is not. + recorded_frames: u64, + /// The recorded frame whose draw list the draw buffer holds, which the + /// next one's opaque draws use; `None` when there is none to use. + list_frame: Option, + /// Per frame in flight, whether its cleaner readback holds a pass's + /// keys. + cleaner_pending: [bool; MAX_FRAMES_IN_FLIGHT], + /// Per frame in flight, whether a traversal filled its request readback; + /// a frame that skipped the pass leaves the last run's requests there. + request_pending: [bool; MAX_FRAMES_IN_FLIGHT], + /// The buffers the GPU passes keep between frames: the node tree, the + /// traversal's request queue, queue metadata and scratch queues, the + /// render list, the nodes' last-drawn frames and the cleaner's output, + /// the draw-count buffer, draw list, draw positions, section visibility + /// and translucent buckets. + nodes: rhi::backend::Buffer, + requests: rhi::backend::Buffer, + queue_meta: rhi::backend::Buffer, + scratch: [rhi::backend::Buffer; 2], + render_list: rhi::backend::Buffer, + node_visibility: rhi::backend::Buffer, + cleaner_ids: rhi::backend::Buffer, + cleaner_keys: rhi::backend::Buffer, + counts: rhi::backend::Buffer, + draws: rhi::backend::Buffer, + positions: rhi::backend::Buffer, + visibility: rhi::backend::Buffer, + translucent: rhi::backend::Buffer, + indirect: IndirectDrawFeatures, + /// The ring of top-level sections: its centre, radius and height + /// range, so the roots change only when it moves. + ring: Option<(i32, i32, i32, i32, i32)>, + view: Option, + bounds: BoundStore, + frame_seq: u64, + /// Sections whose quads and ids are freed at the frame from which no + /// in-flight frame can still draw them. + pending_free: VecDeque<(u64, Uploaded)>, + /// Sections let go of, by the graph's dirty mark then; they join + /// [`Self::pending_free`] once the records changed by then are written, + /// so none on the GPU names them. + unflushed_free: VecDeque<(u64, Uploaded)>, + /// Every model's six faces and their mips, a tile each. + model_atlas: rhi::backend::Texture, + model_sampler: rhi::backend::Sampler, + /// Every model's face data ([`BlockModel`]). + models: rhi::backend::Buffer, + /// Biome tints by kind, then biome. + tints: rhi::backend::Buffer, + /// Models and tints waiting for [`Self::record_model_upload`], which + /// copies them in through `model_staging` so no frame in flight sees + /// them change. + pending_models: BTreeMap, + pending_tints: Option>, + model_staging: Vec, + /// Tiles waiting for [`Self::record_model_upload`]. + pending_tiles: VecDeque, + tile_staging: Vec, +} + +/// Set 1: the arena's quads, the models, the biome tints, the model atlas +/// and its sampler. +const LAYOUT: &[rhi::BindGroupLayoutEntry] = &[ + rhi::BindGroupLayoutEntry::storage(rhi::ShaderStages::VERTEX), + rhi::BindGroupLayoutEntry::storage(rhi::ShaderStages::VERTEX), + rhi::BindGroupLayoutEntry::storage(rhi::ShaderStages::VERTEX), + rhi::BindGroupLayoutEntry::texture(rhi::ShaderStages::FRAGMENT), + rhi::BindGroupLayoutEntry::sampler(rhi::ShaderStages::FRAGMENT), +]; + +/// `distant_scatter.comp`'s set: the frame's list, the records and the +/// nodes. +const SCATTER_LAYOUT: &[rhi::BindGroupLayoutEntry] = + &[rhi::BindGroupLayoutEntry::storage(rhi::ShaderStages::COMPUTE); 3]; + +impl DistantBuffers { + pub fn new(device: &rhi::backend::Device, indirect: IndirectDrawFeatures) -> Self { + // The shared index buffer: corner 1 leads both triangles, so it's the + // provoking vertex the flat attributes come from; the cube after. + let indices: Vec = (0..QUADS_PER_DRAW) + .flat_map(|quad| [1, 2, 0, 1, 3, 2].map(|i| quad * 4 + i)) + .chain(CUBE) + .collect(); + let model_atlas = rhi::backend::Texture::new( + device, + &rhi::TextureDesc { + mip_levels: MODEL_MIP_LEVELS, + ..util::sampled_texture_desc( + "distant_models", + ATLAS_COLUMNS * TILE_WIDTH, + ATLAS_ROWS * TILE_HEIGHT, + ) + }, + ); + let atlas = model_atlas.handle(); + let device_buffer = |name, bytes, usage| { + rhi::backend::Buffer::new( + device, + name, + bytes, + usage | rhi::BufferUsages::COPY_DST, + rhi::MemoryLocation::Device, + ) + }; + let storage = rhi::BufferUsages::STORAGE; + let arena_bytes = MAX_ARENA_BYTES + .min(device.memory_bytes(rhi::MemoryLocation::Device) / 8) + .max(MIN_ARENA_BYTES) + / 8 + * 8; + let arena = device_buffer("distant_quads", arena_bytes, storage); + let sections = device_buffer("distant_sections", SECTIONS_BYTES, storage); + let nodes = device_buffer("distant_nodes", NODE_BYTES, storage); + let requests = device_buffer( + "distant_requests", + REQUESTS_BYTES, + storage | rhi::BufferUsages::COPY_SRC, + ); + let queue_meta = device_buffer( + "distant_queue_meta", + QUEUE_META_BYTES, + storage | rhi::BufferUsages::INDIRECT, + ); + let scratch = ["distant_queue_a", "distant_queue_b"] + .map(|name| device_buffer(name, SCRATCH_BYTES, storage)); + let render_list = device_buffer("distant_render_list", RENDER_LIST_BYTES, storage); + let node_visibility = + device_buffer("distant_node_visibility", NODE_VISIBILITY_BYTES, storage); + let cleaner_ids = device_buffer("distant_cleaner_ids", CLEANER_IDS_BYTES, storage); + let cleaner_keys = device_buffer( + "distant_cleaner_keys", + CLEANER_KEYS_BYTES, + storage | rhi::BufferUsages::COPY_SRC, + ); + let counts = device_buffer( + "distant_draw_counts", + COUNTS_BYTES, + storage | rhi::BufferUsages::INDIRECT | rhi::BufferUsages::COPY_SRC, + ); + let draws = device_buffer( + "distant_draws", + DRAWS_BYTES, + storage | rhi::BufferUsages::INDIRECT, + ); + let positions = device_buffer("distant_draw_positions", POSITIONS_BYTES, storage); + let visibility = device_buffer("distant_visibility", VISIBILITY_BYTES, storage); + let translucent = device_buffer("distant_translucent_draws", TRANSLUCENT_BYTES, storage); + let models = device_buffer("distant_block_models", MODELS_BYTES, storage); + let tints = device_buffer("distant_biome_tints", TINTS_BYTES, storage); + device.submit(|encoder| { + encoder.transition_textures(&[atlas_transition( + atlas, + rhi::TextureUse::Undefined, + rhi::TextureUse::CopyDst, + )]); + encoder.clear_texture(atlas, MODEL_ATLAS_RANGE, [0.0; 4]); + encoder.transition_textures(&[atlas_transition( + atlas, + rhi::TextureUse::CopyDst, + rhi::TextureUse::ShaderRead, + )]); + // No nodes, none drawn, nothing seen, nothing to draw before the + // first frame. + encoder.fill_buffer(nodes.handle(), 0, NODE_BYTES, u32::MAX); + encoder.fill_buffer(node_visibility.handle(), 0, NODE_VISIBILITY_BYTES, u32::MAX); + encoder.fill_buffer(visibility.handle(), 0, VISIBILITY_BYTES, 0); + encoder.fill_buffer(counts.handle(), 0, COUNTS_BYTES, 0); + encoder.fill_buffer(render_list.handle(), 0, 4, 0); + encoder.fill_buffer(models.handle(), 0, MODELS_BYTES, 0); + encoder.fill_buffer(tints.handle(), 0, TINTS_BYTES, 0); + }); + // The model atlas's sampler: nearest within a level, linear between + // the tiles' mip levels. + let model_sampler = rhi::backend::create_sampler( + device, + &rhi::SamplerDesc { + mipmap_filter: rhi::FilterMode::Linear, + lod_max_clamp: (MODEL_MIP_LEVELS - 1) as f32, + ..rhi::SamplerDesc::NEAREST + }, + ); + let layout = rhi::backend::create_bind_group_layout(device, LAYOUT); + let group = device.create_bind_group( + layout, + &[ + util::storage_binding(0, &arena, arena_bytes), + util::storage_binding(1, &models, MODELS_BYTES), + util::storage_binding(2, &tints, TINTS_BYTES), + (3, rhi::backend::BindingResource::Texture(atlas)), + (4, rhi::backend::BindingResource::Sampler(model_sampler)), + ], + ); + let per_frame = + |bytes, usage, name| util::create_per_frame_host_buffers(device, bytes, usage, name); + let readbacks = |name, bytes| -> Vec { + (0..MAX_FRAMES_IN_FLIGHT) + .map(|_| { + rhi::backend::Buffer::new( + device, + name, + bytes, + rhi::BufferUsages::COPY_DST, + rhi::MemoryLocation::Readback, + ) + }) + .collect() + }; + let scatter_staging = per_frame(SCATTER_BYTES, storage, "distant_scatter"); + // The record count, as its push constant. + let scatter = util::ComputePass::new( + device, + "distant scatter", + shader::include_shader!("distant_scatter.comp"), + SCATTER_LAYOUT, + Some(rhi::PushConstants { + visibility: rhi::ShaderStages::COMPUTE, + size: 4, + }), + MAX_FRAMES_IN_FLIGHT, + |frame| { + vec![ + util::storage_binding(0, &scatter_staging[frame], SCATTER_BYTES), + util::storage_binding(1, §ions, SECTIONS_BYTES), + util::storage_binding(2, &nodes, NODE_BYTES), + ] + }, + ); + Self { + layout, + group, + quads: Tlsf::new((arena_bytes / 8) as u32), + arena, + sections, + ids: Tlsf::new(MAX_SECTIONS), + meshes: HashMap::new(), + index_buffer: rhi::backend::Buffer::with_data( + device, + "distant_quad_indices", + bytemuck::cast_slice(&indices), + rhi::BufferUsages::INDEX, + ), + pending: VecDeque::new(), + waiting: HashMap::new(), + graph: NodeGraph::new(), + quad_staging: per_frame( + STAGING_BYTES, + rhi::BufferUsages::COPY_SRC, + "distant_quad_staging", + ), + scatter_staging, + scatter, + top_ids: per_frame(TOP_IDS_BYTES, storage, "distant_top_nodes"), + queue_meta_staging: per_frame( + QUEUE_META_BYTES, + rhi::BufferUsages::COPY_SRC, + "distant_queue_meta_staging", + ), + visibility_sets: per_frame(VISIBILITY_SETS_BYTES, storage, "distant_visibility_sets"), + request_readbacks: readbacks("distant_request_readback", REQUESTS_BYTES), + cleaner_readbacks: readbacks("distant_cleaner_readback", CLEANER_KEYS_BYTES), + cleaner_pending: [false; MAX_FRAMES_IN_FLIGHT], + request_pending: [false; MAX_FRAMES_IN_FLIGHT], + count_readbacks: readbacks("distant_count_readback", COUNTS_BYTES), + last_counts: [0; COUNT_WORDS], + slots: [0; 3], + recorded_frames: 0, + list_frame: None, + nodes, + requests, + queue_meta, + scratch, + render_list, + node_visibility, + cleaner_ids, + cleaner_keys, + counts, + draws, + positions, + visibility, + translucent, + indirect, + ring: None, + view: None, + bounds: BoundStore::new(device), + frame_seq: 0, + pending_free: VecDeque::new(), + unflushed_free: VecDeque::new(), + model_atlas, + model_sampler, + models, + tints, + pending_tiles: VecDeque::new(), + pending_models: BTreeMap::new(), + pending_tints: None, + model_staging: per_frame( + MODELS_BYTES + TINTS_BYTES, + rhi::BufferUsages::COPY_SRC, + "distant_model_data_staging", + ), + tile_staging: per_frame( + (TILE_BYTES * TILES_PER_FRAME) as u64, + rhi::BufferUsages::COPY_SRC, + "distant_model_staging", + ), + } + } + + pub fn layout(&self) -> rhi::backend::BindGroupLayout { + self.layout + } + + pub fn draw_buffers(&self) -> DrawBuffers<'_> { + DrawBuffers { + sections: &self.sections, + counts: &self.counts, + draws: &self.draws, + positions: &self.positions, + visibility: &self.visibility, + translucent: &self.translucent, + render_list: &self.render_list, + nodes: &self.nodes, + requests: &self.requests, + queue_meta: &self.queue_meta, + scratch: self.scratch.each_ref(), + node_visibility: &self.node_visibility, + cleaner_ids: &self.cleaner_ids, + cleaner_keys: &self.cleaner_keys, + } + } + + /// The top-level nodes' ids, the traversal's first iteration's source. + pub fn top_ids(&self, frame: usize) -> &rhi::backend::Buffer { + &self.top_ids[frame] + } + + pub fn top_count(&self) -> u32 { + self.graph.top_ids().len() as u32 + } + + /// The ids whose last-drawn frame the visibility-set pass sets. + pub fn visibility_sets(&self, frame: usize) -> &rhi::backend::Buffer { + &self.visibility_sets[frame] + } + + /// The quads' indices, then the cube's from [`CUBE_FIRST_INDEX`]. + pub fn index_buffer(&self) -> rhi::backend::BufferHandle { + self.index_buffer.handle() + } + + /// The frame's id as the GPU passes see it. + pub fn frame_id(&self) -> u32 { + self.frame_seq as u32 & !SEEN_BEFORE_BIT + } + + /// The render distance squared, in blocks. + pub fn render_distance_sq(&self) -> f32 { + self.view + .as_ref() + .map_or(0.0, |view| (view.radius * view.radius) as f32) + } + + /// Requests the traversal may make this frame: fewer the more wait on + /// the worker. + pub fn request_limit(&self) -> u32 { + let room = ((TARGET_REQUESTS - self.graph.in_flight() as f64) / TARGET_REQUESTS).max(0.0); + ((room * room * f64::from(MAX_REQUESTS)).ceil() as u32).min(MAX_REQUESTS) + } + + /// Writes the changed models and tints, then the waiting model tiles + /// into the atlas, as many as fit this frame's staging. + pub fn record_model_upload(&mut self, encoder: &rhi::backend::CommandEncoder, frame: usize) { + self.record_model_data(encoder, frame); + if self.pending_tiles.is_empty() { + return; + } + let staging = &mut self.tile_staging[frame]; + let mut copies = Vec::new(); + let mut offset = 0; + for tile in self + .pending_tiles + .drain(..TILES_PER_FRAME.min(self.pending_tiles.len())) + { + let (x, y) = ( + u32::from(tile.id) % ATLAS_COLUMNS * TILE_WIDTH, + u32::from(tile.id) / ATLAS_COLUMNS * TILE_HEIGHT, + ); + for (level, pixels) in (0..).zip(&tile.levels) { + staging.write_at(offset, pixels); + copies.push(rhi::TexelCopy { + buffer_offset: offset as u64, + mip_level: level, + layer: 0, + origin: [x >> level, y >> level], + width: TILE_WIDTH >> level, + height: TILE_HEIGHT >> level, + }); + offset += pixels.len(); + } + } + let atlas = self.model_atlas.handle(); + encoder.transition_textures(&[atlas_transition( + atlas, + rhi::TextureUse::ShaderRead, + rhi::TextureUse::CopyDst, + )]); + encoder.copy_buffer_to_texture(staging.handle(), atlas, &copies); + encoder.transition_textures(&[atlas_transition( + atlas, + rhi::TextureUse::CopyDst, + rhi::TextureUse::ShaderRead, + )]); + } + + /// The changed models, then the tints, through `frame`'s staging. + fn record_model_data(&mut self, encoder: &rhi::backend::CommandEncoder, frame: usize) { + let models = std::mem::take(&mut self.pending_models); + let tints = self.pending_tints.take(); + if models.is_empty() && tints.is_none() { + return; + } + let staging = &mut self.model_staging[frame]; + let model_bytes = size_of::(); + let mut copies = Vec::with_capacity(models.len()); + for (i, (id, model)) in models.iter().enumerate() { + staging.write_at(i * model_bytes, bytemuck::bytes_of(model)); + copies.push(rhi::BufferCopy { + src_offset: (i * model_bytes) as u64, + dst_offset: (usize::from(*id) * model_bytes) as u64, + size: model_bytes as u64, + }); + } + encoder.transition_buffers(rhi::BufferUse::ShaderRead, rhi::BufferUse::CopyDst); + if !copies.is_empty() { + encoder.copy_buffer(staging.handle(), self.models.handle(), &copies); + } + if let Some(tints) = tints { + staging.write_at(MODELS_BYTES as usize, bytemuck::cast_slice(&tints)); + encoder.copy_buffer( + staging.handle(), + self.tints.handle(), + &[rhi::BufferCopy { + src_offset: MODELS_BYTES, + dst_offset: 0, + size: TINTS_BYTES, + }], + ); + } + encoder.transition_buffers(rhi::BufferUse::CopyDst, rhi::BufferUse::ShaderRead); + } + + /// Carries the pending sections up, their quads into the arena through + /// `frame`'s staging and their records through its scatter list, as many + /// as fit, the rest next frame; then the changed nodes through the same + /// list, but those naming a section still pending, which wait with it; + /// then the sections no node record names any more are freed. Call once + /// a recorded frame. + pub fn record_uploads(&mut self, encoder: &rhi::backend::CommandEncoder, frame: usize) { + self.recorded_frames += 1; + let mut copies = Vec::new(); + let mut records: Vec<(u32, [u32; 4])> = Vec::new(); + let mut used = 0; + while let Some(next) = self.pending.front() { + let bytes = next.quads.len() * 8; + if used + bytes > STAGING_BYTES as usize || records.len() + 2 > SCATTER_RECORDS { + break; + } + let Pending { + uploaded, + quads, + record, + } = self.pending.pop_front().expect("peeked"); + self.quad_staging[frame].write_at(used, bytemuck::cast_slice(&quads)); + copies.push(rhi::BufferCopy { + src_offset: used as u64, + dst_offset: u64::from(uploaded.ptr) * 8, + size: bytes as u64, + }); + used += bytes; + for (i, half) in (0..).zip(record) { + records.push((uploaded.id * 2 + i, half)); + } + self.meshes.insert(uploaded.id, uploaded); + } + if !copies.is_empty() { + encoder.copy_buffer( + self.quad_staging[frame].handle(), + self.arena.handle(), + &copies, + ); + encoder.transition_buffers(rhi::BufferUse::CopyDst, rhi::BufferUse::ShaderRead); + } + // What fits the list; the rest of the nodes wait for the next frame. + let pending: HashSet = self.pending.iter().map(|p| p.uploaded.id).collect(); + records.extend( + self.graph + .take_dirty(SCATTER_RECORDS.saturating_sub(records.len()), |id| { + pending.contains(&id) + }) + .into_iter() + .map(|(id, record)| (id | NODE_SCATTER_BIT, record)), + ); + let written = self.graph.written_through(); + while let Some(&(mark, section)) = self.unflushed_free.front() + && mark <= written + { + self.unflushed_free.pop_front(); + self.free_later(section); + } + if records.is_empty() { + return; + } + // Chunks of four indices, then their four records; the count stops + // the shader short of a last chunk's padding. + let mut list: Vec<[u32; 4]> = Vec::with_capacity(records.len().div_ceil(4) * 5); + for chunk in records.chunks(4) { + let mut indices = [0; 4]; + let mut data = [[0; 4]; 4]; + for (i, &(index, record)) in chunk.iter().enumerate() { + indices[i] = index; + data[i] = record; + } + list.push(indices); + list.extend(data); + } + self.scatter_staging[frame].write_at(0, bytemuck::cast_slice(&list)); + let count = records.len() as u32; + encoder.dispatch( + &self.scatter.pipeline, + self.scatter.set(frame), + &count.to_le_bytes(), + [count.div_ceil(LIST_GROUP), 1, 1], + ); + encoder.transition_buffers_many(&[ + (rhi::BufferUse::StorageWrite, rhi::BufferUse::Storage), + (rhi::BufferUse::StorageWrite, rhi::BufferUse::ShaderRead), + ]); + } + + /// Before the traversal: this frame's top-level ids (those whose records + /// are written) and queue metadata (the first iteration over the roots, + /// the rest empty), the render list and request queue emptied; how many + /// roots. + pub fn record_traversal_setup( + &mut self, + encoder: &rhi::backend::CommandEncoder, + frame: usize, + ) -> u32 { + let top = self.graph.written_top_ids(); + let count = top.len() as u32; + self.top_ids[frame].write_at(0, bytemuck::cast_slice(&top)); + let mut meta = [[0u32, 1, 1, 0]; LEVELS as usize]; + meta[0] = [count.div_ceil(TRAVERSAL_GROUP), 1, 1, count]; + self.queue_meta_staging[frame].write_at(0, bytemuck::cast_slice(&meta)); + encoder.transition_buffers_many(&[ + (rhi::BufferUse::DrawInput, rhi::BufferUse::CopyDst), + (rhi::BufferUse::Storage, rhi::BufferUse::CopyDst), + ]); + encoder.copy_buffer( + self.queue_meta_staging[frame].handle(), + self.queue_meta.handle(), + &[rhi::BufferCopy::from_start(QUEUE_META_BYTES)], + ); + encoder.fill_buffer(self.render_list.handle(), 0, 4, 0); + encoder.fill_buffer(self.requests.handle(), 0, 8, 0); + encoder.transition_buffers_many(&[ + (rhi::BufferUse::CopyDst, rhi::BufferUse::Storage), + (rhi::BufferUse::CopyDst, rhi::BufferUse::DrawInput), + ]); + count + } + + /// After the traversal: its requests into this frame's readback, read + /// by [`Self::begin_frame`] once the frame is done. + pub fn record_request_readback( + &mut self, + encoder: &rhi::backend::CommandEncoder, + frame: usize, + ) { + self.request_pending[frame] = true; + record_readback( + encoder, + &self.requests, + &self.request_readbacks[frame], + REQUESTS_BYTES, + ); + } + + /// Writes this frame's visibility sets: the nodes allocated and freed + /// since last time, as many as fit, the rest next frame; how many. + pub fn write_visibility_sets(&mut self, frame: usize) -> u32 { + let sets = &mut self.graph.visibility_sets; + let ids: Vec = sets.iter().copied().take(MAX_VISIBILITY_SETS).collect(); + for id in &ids { + sets.remove(id); + } + self.visibility_sets[frame].write_at(0, bytemuck::cast_slice(&ids)); + ids.len() as u32 + } + + /// Before the cleaner's sort, when it should run (less than an eighth of + /// the arena free, a fraction rather than a fixed amount since the arena + /// is sized to the device): its output reset to the sentinel, an id no + /// node has; the sort's workgroups. + pub fn record_cleaner_setup( + &mut self, + encoder: &rhi::backend::CommandEncoder, + frame: usize, + ) -> Option { + let capacity = self.quads.capacity(); + let groups = self.graph.end_id().div_ceil(CLEANER_SORT_GROUP); + if capacity - self.quads.used() >= capacity / 8 || groups == 0 { + return None; + } + encoder.transition_buffers(rhi::BufferUse::Storage, rhi::BufferUse::CopyDst); + encoder.fill_buffer( + self.cleaner_ids.handle(), + 0, + CLEANER_IDS_BYTES, + CLEANER_SENTINEL, + ); + encoder.transition_buffers(rhi::BufferUse::CopyDst, rhi::BufferUse::Storage); + self.cleaner_pending[frame] = true; + Some(groups) + } + + /// Without an indirect count, before the command generation: zeroes + /// the slots each region draws ([`draw_slots`]) so the unwritten ones + /// draw nothing, as the chunk renderer's cull does. + pub fn record_draw_slots(&mut self, encoder: &rhi::backend::CommandEncoder) { + if self.indirect.count { + return; + } + let stride = size_of::() as u64; + encoder.transition_buffers(rhi::BufferUse::DrawInput, rhi::BufferUse::CopyDst); + self.slots = draw_slots(&self.last_counts); + for region in Region::ALL { + let (first, ..) = region.layout(); + encoder.fill_buffer( + self.draws.handle(), + u64::from(first) * stride, + u64::from(self.slots[region as usize]) * stride, + 0, + ); + } + } + + /// After the command generation: this frame's draw list is the one the + /// next frame's opaque draws use; without an indirect count, the draw + /// counts into this frame's readback. + pub fn finish_list(&mut self, encoder: &rhi::backend::CommandEncoder, frame: usize) { + self.list_frame = Some(self.recorded_frames); + if !self.indirect.count { + record_readback( + encoder, + &self.counts, + &self.count_readbacks[frame], + COUNTS_BYTES, + ); + } + } + + /// After the cleaner's results: the keys to drop into this frame's + /// readback. + pub fn record_cleaner_readback(&self, encoder: &rhi::backend::CommandEncoder, frame: usize) { + record_readback( + encoder, + &self.cleaner_keys, + &self.cleaner_readbacks[frame], + CLEANER_KEYS_BYTES, + ); + } + + pub fn bound_layout(&self) -> rhi::backend::BindGroupLayout { + self.bounds.layout() + } + + /// The bound store with the frame's buffer brought up to date. + pub fn bounds(&mut self, frame: usize) -> &BoundStore { + self.bounds.flush(frame); + &self.bounds + } + + /// Reclaims the ranges and buffers no in-flight frame reads any more, + /// and takes up the requests the traversal made, and the meshes the + /// cleaner picked, the last time this frame slot ran. Call once a + /// frame, after its fence wait. + pub fn begin_frame(&mut self, device: &rhi::backend::Device, frame: usize) { + self.frame_seq += 1; + self.bounds.free_retired(device, self.frame_seq); + while let Some(&(at, section)) = self.pending_free.front() + && at <= self.frame_seq + { + self.pending_free.pop_front(); + self.ids.free(section.id); + self.quads.free(section.ptr); + } + self.place_waiting(); + if std::mem::take(&mut self.request_pending[frame]) { + let keys: Vec = read_words( + &mut self.request_readbacks[frame], + REQUESTS_BYTES, + |words| { + let count = (words[0] as usize).min(MAX_REQUESTS as usize); + packed_keys(&words[2..2 + count * 2]).collect() + }, + ); + for key in keys { + self.graph.process_request(SectionKey::unpack(key)); + } + } + if !self.indirect.count { + self.last_counts = + read_words(&mut self.count_readbacks[frame], COUNTS_BYTES, |words| { + words.try_into().expect("the whole draw-count buffer") + }); + } + if std::mem::take(&mut self.cleaner_pending[frame]) { + let picks: Vec<(u32, u64)> = read_words( + &mut self.cleaner_readbacks[frame], + CLEANER_KEYS_BYTES, + |words| { + words + .chunks(4) + .map(|pick| (pick[2], packed_key(pick[0], pick[1]))) + .collect() + }, + ); + for (id, key) in picks { + let key = SectionKey::unpack(key); + if self.graph.holds(id, key) { + self.graph.remove_node_geometry(key); + } + } + self.free_released(); + } + } + + /// Frees `section`'s quads and id once no frame draws them. + fn free_later(&mut self, section: Uploaded) { + let at = self.frame_seq + MAX_FRAMES_IN_FLIGHT as u64; + self.pending_free.push_back((at, section)); + } + + /// Frees the meshes the graph let go of once no node record names them + /// and no frame draws them. + fn free_released(&mut self) { + let mark = self.graph.dirty_mark(); + for id in std::mem::take(&mut self.graph.freed) { + let section = if let Some(section) = self.meshes.remove(&id) { + section + } else if let Some(at) = self.pending.iter().position(|p| p.uploaded.id == id) { + self.pending.remove(at).expect("found").uploaded + } else { + continue; + }; + self.unflushed_free.push_back((mark, section)); + } + } + + /// Drops every section, as on a dimension change. The bounds stay: they + /// track the chunk store, which [`Self::clear_bounds`] follows. Nothing + /// draws until new roots' records are written, so the meshes go free. + pub fn clear(&mut self) { + self.list_frame = None; + self.cleaner_pending = [false; MAX_FRAMES_IN_FLIGHT]; + self.waiting.clear(); + self.graph.clear(); + self.free_released(); + for (_, section) in std::mem::take(&mut self.unflushed_free) { + self.free_later(section); + } + self.ring = None; + self.view = None; + } + + pub fn clear_bounds(&mut self) { + self.bounds.clear(); + } + + /// Which of column `pos`'s sections are compiled (bit `i` for the `i`th + /// from the dimension's bottom, `min_section`), bounding distant terrain + /// there. + pub fn set_column_sections( + &mut self, + device: &rhi::backend::Device, + pos: ChunkPos, + sections: u32, + min_section: i32, + ) { + self.bounds + .set_column(device, self.frame_seq, pos, sections, min_section); + } + + /// Which sections the occlusion graph shows, per column, as the chunk + /// cull draws them. + pub fn set_visibility(&mut self, device: &rhi::backend::Device, vis: &HashMap) { + self.bounds.set_visibility(device, self.frame_seq, vis); + } + + /// Where the player is and how far to draw; `None` draws nothing. + pub fn set_view(&mut self, view: Option) { + if self.view != view { + if view.as_ref().is_none_or(|v| { + self.view + .as_ref() + .is_some_and(|last| last.dimension != v.dimension) + }) { + self.clear(); + } + self.view = view; + } + } + + /// Takes the worker's updates: the models and tints up, the sections + /// into the graph and the arena as room allows. + pub fn upload(&mut self, updates: impl IntoIterator) { + for update in updates { + if self + .view + .as_ref() + .is_none_or(|view| view.dimension != update.dimension) + { + continue; + } + if let Some(tints) = update.tints { + self.pending_tints = Some(tints); + } + for tile in update.tiles { + self.pending_models.insert(tile.id, tile.model); + self.pending_tiles.push_back(tile); + } + for section in update.sections { + self.waiting.insert(section.key, section); + } + } + self.place_waiting(); + } + + /// Takes the waiting results whose quads fit the arena into the graph. + fn place_waiting(&mut self) { + if self.waiting.is_empty() { + return; + } + let keys: Vec = self.waiting.keys().copied().collect(); + for key in keys { + let quads = self.waiting[&key].quads.len() as u32; + if quads > 0 && !self.quads.fits(quads) { + continue; + } + let section = self.waiting.remove(&key).expect("listed"); + let (quads, ids, pending) = (&mut self.quads, &mut self.ids, &mut self.pending); + self.graph.process_geometry_result(section, &mut |section| { + place(quads, ids, pending, section) + }); + } + self.free_released(); + } + + /// The sections the graph asked the worker for since last time, and + /// those it dropped. + pub fn take_requests(&mut self) -> (Vec, Vec) { + ( + std::mem::take(&mut self.graph.watch), + std::mem::take(&mut self.graph.unwatch), + ) + } + + /// Whether the finest section holding block column `x`/`z` is meshed + /// with something in it, at any height. + #[cfg(feature = "test-harness")] + pub fn has_column(&self, x: i32, z: i32) -> bool { + self.graph.has_column(x, z) + } + + /// The top-level nodes are a ring of sections around `eye`, over the + /// dimension's height, remade when it moves. + pub fn update_roots(&mut self, eye: DVec3) { + let Some(view) = self.view.clone() else { + return; + }; + let top = LEVELS - 1; + let top_width = (SIDE as i32) << top; + let ring = (view.radius / f64::from(top_width)).ceil() as i32 + 1; + let (cx, cz) = ( + (eye.x.floor() as i32).div_euclid(top_width), + (eye.z.floor() as i32).div_euclid(top_width), + ); + let (min_sec, max_sec) = ( + view.min_y.div_euclid(top_width), + (view.min_y + view.height - 1).div_euclid(top_width), + ); + if self.ring == Some((cx, cz, ring, min_sec, max_sec)) { + return; + } + self.ring = Some((cx, cz, ring, min_sec, max_sec)); + let mut roots = HashSet::new(); + for x in cx - ring..=cx + ring { + for z in cz - ring..=cz + ring { + for y in min_sec..=max_sec { + roots.insert(SectionKey { + level: top, + x, + y, + z, + }); + } + } + } + assert!(roots.len() <= MAX_ROOTS, "too many top-level sections"); + self.graph.set_roots(&roots); + self.free_released(); + } + + /// Issues one region of the draw list the GPU built: an indirect draw + /// whose count the generation wrote, or, without an indirect count, over + /// the zeroed slots of [`Self::record_draw_slots`], one call each without + /// multi-draw. The caller has bound the matching distant pipeline and its + /// sets. + pub fn draw(&self, pass: &rhi::backend::RenderPassEncoder, region: Region) { + // The opaque region is last frame's list, of no use after a clear or + // a frame that built none. + if matches!(region, Region::Opaque) + && self + .list_frame + .is_none_or(|built| built + 1 != self.recorded_frames) + { + return; + } + let (first, count_offset, max_draws) = region.layout(); + pass.set_bind_group(1, self.group); + pass.set_index_buffer(self.index_buffer.handle()); + let stride = size_of::() as u32; + let draws = self.draws.handle(); + let offset = u64::from(first * stride); + let slots = self.slots[region as usize]; + if self.indirect.count { + pass.draw_indexed_indirect_count( + draws, + offset, + self.counts.handle(), + count_offset, + max_draws, + stride, + ); + } else if self.indirect.multi_draw { + pass.draw_indexed_indirect(draws, offset, slots, stride); + } else { + for i in 0..slots { + pass.draw_indexed_indirect(draws, offset + u64::from(i * stride), 1, stride); + } + } + } + + pub fn destroy(&mut self, device: &rhi::backend::Device) { + self.group.destroy(device); + let [scratch_a, scratch_b] = &mut self.scratch; + for buffer in [ + &mut self.arena, + &mut self.sections, + &mut self.index_buffer, + &mut self.nodes, + &mut self.requests, + &mut self.queue_meta, + scratch_a, + scratch_b, + &mut self.render_list, + &mut self.node_visibility, + &mut self.cleaner_ids, + &mut self.cleaner_keys, + &mut self.counts, + &mut self.draws, + &mut self.positions, + &mut self.visibility, + &mut self.translucent, + &mut self.models, + &mut self.tints, + ] { + std::mem::take(buffer).destroy(device); + } + for buffers in [ + &mut self.quad_staging, + &mut self.scatter_staging, + &mut self.top_ids, + &mut self.queue_meta_staging, + &mut self.visibility_sets, + &mut self.request_readbacks, + &mut self.cleaner_readbacks, + &mut self.count_readbacks, + &mut self.tile_staging, + &mut self.model_staging, + ] { + util::destroy_buffers(device, buffers); + } + self.scatter.destroy(device); + self.bounds.destroy(device); + self.model_sampler.destroy(device); + std::mem::take(&mut self.model_atlas).destroy(device); + self.layout.destroy(device); + } +} + +/// Copies `source` into `readback` for the CPU to read once the frame is +/// done. +fn record_readback( + encoder: &rhi::backend::CommandEncoder, + source: &rhi::backend::Buffer, + readback: &rhi::backend::Buffer, + bytes: u64, +) { + encoder.transition_buffers(rhi::BufferUse::StorageWrite, rhi::BufferUse::CopySrc); + encoder.copy_buffer( + source.handle(), + readback.handle(), + &[rhi::BufferCopy::from_start(bytes)], + ); + encoder.transition_buffers(rhi::BufferUse::CopyDst, rhi::BufferUse::HostRead); +} + +/// The first `bytes` of `readback`, as `u32`s. +fn read_words( + readback: &mut rhi::backend::Buffer, + bytes: u64, + read: impl FnOnce(&[u32]) -> R, +) -> R { + readback.read(|data| read(bytemuck::cast_slice(&data[..bytes as usize]))) +} + +/// The packed keys in `words`, a `uvec2` each. +fn packed_keys(words: &[u32]) -> impl Iterator + '_ { + words.chunks(2).map(|key| packed_key(key[0], key[1])) +} + +/// A section key the GPU wrote as its high word, then its low. +fn packed_key(high: u32, low: u32) -> u64 { + u64::from(high) << 32 | u64::from(low) +} + +/// Places `section`'s quads in the arena and queues their upload; its id, or +/// `None` when out of ids. +fn place( + quads: &mut Tlsf, + ids: &mut Tlsf, + pending: &mut VecDeque, + section: BuiltSection, +) -> Option { + let ptr = quads.alloc(section.quads.len() as u32)?; + let Some(id) = ids.alloc(1) else { + quads.free(ptr); + return None; + }; + pending.push_back(Pending { + uploaded: Uploaded { id, ptr }, + record: section_record(§ion, ptr), + quads: section.quads, + }); + Some(id) +} + +/// The chunk sections the chunk renderer draws (compiled, and shown by the +/// occlusion graph) for the bound pass, as boxes in a buffer the pass +/// instances over, the last slot moving into a freed one. The columns are +/// kept across frames rather than streamed anew each frame, and each run of +/// stacked sections is one box, whose far faces are the run's. +pub(crate) struct BoundStore { + layout: rhi::backend::BindGroupLayout, + /// One per frame in flight, written as its frame records the bound + /// pass, so a frame still drawing never reads a moved box. + buffers: Vec, + groups: Vec, + /// Per frame in flight, the slots changed since its buffer was written. + dirty: [Option<(u32, u32)>; MAX_FRAMES_IN_FLIGHT], + capacity: u32, + /// Each box's section coordinates and its height in sections. + slots: Vec<[i32; 4]>, + /// The slot of the box starting at these section coordinates. + index: HashMap<[i32; 3], u32>, + /// Per column, which of its sections are compiled and which the graph + /// shows (a bit per section from `min_section` up). + columns: HashMap, + min_section: i32, + /// Buffers in-flight frames may still read, by the frame they're free + /// from. + retired: VecDeque<(u64, Vec, Vec)>, +} + +impl BoundStore { + fn new(device: &rhi::backend::Device) -> Self { + let layout = rhi::backend::create_bind_group_layout( + device, + &[rhi::BindGroupLayoutEntry::storage( + rhi::ShaderStages::VERTEX, + )], + ); + let (buffers, groups) = Self::make_buffers(device, layout, BOUND_CAPACITY); + Self { + layout, + buffers, + groups, + dirty: [None; MAX_FRAMES_IN_FLIGHT], + capacity: BOUND_CAPACITY, + slots: Vec::new(), + index: HashMap::new(), + columns: HashMap::new(), + min_section: 0, + retired: VecDeque::new(), + } + } + + fn make_buffers( + device: &rhi::backend::Device, + layout: rhi::backend::BindGroupLayout, + capacity: u32, + ) -> (Vec, Vec) { + let bytes = u64::from(capacity) * 16; + (0..MAX_FRAMES_IN_FLIGHT) + .map(|_| { + let buffer = rhi::backend::Buffer::new( + device, + "distant_chunk_bounds", + bytes, + rhi::BufferUsages::STORAGE, + rhi::MemoryLocation::Host, + ); + let group = device.create_bind_group( + layout, + &[(0, rhi::backend::BindingResource::storage(&buffer, bytes))], + ); + (buffer, group) + }) + .unzip() + } + + pub fn layout(&self) -> rhi::backend::BindGroupLayout { + self.layout + } + + pub fn group(&self, frame: usize) -> rhi::backend::BindGroup { + self.groups[frame] + } + + pub fn count(&self) -> u32 { + self.slots.len() as u32 + } + + /// Writes the frame's buffer what changed since it last drew. + fn flush(&mut self, frame: usize) { + let Some((lo, hi)) = self.dirty[frame].take() else { + return; + }; + let (lo, hi) = (lo as usize, (hi as usize).min(self.slots.len())); + if lo < hi { + self.buffers[frame].write_at(lo * 16, bytemuck::cast_slice(&self.slots[lo..hi])); + } + } + + fn mark(&mut self, slot: u32) { + for dirty in &mut self.dirty { + *dirty = Some(match *dirty { + Some((lo, hi)) => (lo.min(slot), hi.max(slot + 1)), + None => (slot, slot + 1), + }); + } + } + + /// Which of column `pos`'s sections are compiled. + fn set_column( + &mut self, + device: &rhi::backend::Device, + frame_seq: u64, + pos: ChunkPos, + compiled: u32, + min_section: i32, + ) { + self.min_section = min_section; + let (_, visible) = self.column(pos); + self.update(device, frame_seq, pos, compiled, visible); + } + + /// Column `pos`'s compiled and shown sections; none and all for one not + /// listed. + fn column(&self, pos: ChunkPos) -> (u32, u32) { + self.columns.get(&pos).copied().unwrap_or((0, u32::MAX)) + } + + /// Sets column `pos`'s compiled and shown sections, re-boxing what + /// changed. + fn update( + &mut self, + device: &rhi::backend::Device, + frame_seq: u64, + pos: ChunkPos, + compiled: u32, + visible: u32, + ) { + let (was_compiled, was_visible) = self.column(pos); + self.rebox( + device, + frame_seq, + pos, + was_compiled & was_visible, + compiled & visible, + ); + if compiled == 0 { + self.columns.remove(&pos); + } else { + self.columns.insert(pos, (compiled, visible)); + } + } + + /// Which sections the occlusion graph shows, per column; a column it + /// doesn't list shows every section, as the chunk cull takes it. + fn set_visibility( + &mut self, + device: &rhi::backend::Device, + frame_seq: u64, + vis: &HashMap, + ) { + let changed: Vec<(ChunkPos, u32, u32)> = self + .columns + .iter() + .filter_map(|(&pos, &(compiled, was))| { + let visible = vis.get(&pos).copied().unwrap_or(u32::MAX); + (visible != was).then_some((pos, compiled, visible)) + }) + .collect(); + for (pos, compiled, visible) in changed { + self.update(device, frame_seq, pos, compiled, visible); + } + } + + /// Replaces column `pos`'s boxes for the sections `was` drawn with those + /// for `now`. + fn rebox( + &mut self, + device: &rhi::backend::Device, + frame_seq: u64, + pos: ChunkPos, + was: u32, + now: u32, + ) { + if was == now { + return; + } + let min_section = self.min_section; + let key = |start| [pos.x, min_section + start, pos.z]; + for (start, _) in runs(was) { + self.remove(key(start)); + } + for (start, height) in runs(now) { + self.add(device, frame_seq, key(start), height); + } + } + + /// A box of `height` sections from the section at `key` up. + fn add(&mut self, device: &rhi::backend::Device, frame_seq: u64, key: [i32; 3], height: i32) { + if self.count() == self.capacity { + self.capacity *= 2; + let (buffers, groups) = Self::make_buffers(device, self.layout, self.capacity); + let old = ( + frame_seq + MAX_FRAMES_IN_FLIGHT as u64, + std::mem::replace(&mut self.buffers, buffers), + std::mem::replace(&mut self.groups, groups), + ); + self.retired.push_back(old); + self.dirty = [Some((0, self.count())); MAX_FRAMES_IN_FLIGHT]; + } + let slot = self.count(); + let [x, y, z] = key; + self.slots.push([x, y, z, height]); + self.index.insert(key, slot); + self.mark(slot); + } + + fn remove(&mut self, key: [i32; 3]) { + let Some(slot) = self.index.remove(&key) else { + return; + }; + let last = self.slots.pop().expect("a slot was indexed"); + if slot < self.count() { + self.slots[slot as usize] = last; + let [x, y, z, _] = last; + self.index.insert([x, y, z], slot); + self.mark(slot); + } + } + + fn clear(&mut self) { + self.slots.clear(); + self.index.clear(); + self.columns.clear(); + } + + fn free_retired(&mut self, device: &rhi::backend::Device, frame_seq: u64) { + while let Some(&(at, ..)) = self.retired.front() + && at <= frame_seq + { + let (_, buffers, groups) = self.retired.pop_front().expect("peeked"); + Self::destroy_buffers(device, buffers, groups); + } + } + + fn destroy_buffers( + device: &rhi::backend::Device, + buffers: Vec, + groups: Vec, + ) { + for group in groups { + group.destroy(device); + } + for buffer in buffers { + buffer.destroy(device); + } + } + + fn destroy(&mut self, device: &rhi::backend::Device) { + self.free_retired(device, u64::MAX); + Self::destroy_buffers( + device, + std::mem::take(&mut self.buffers), + std::mem::take(&mut self.groups), + ); + self.layout.destroy(device); + } +} + +/// The runs of set bits in `mask`, as (first bit, length). +fn runs(mut mask: u32) -> impl Iterator { + std::iter::from_fn(move || { + if mask == 0 { + return None; + } + let start = mask.trailing_zeros(); + let len = (mask >> start).trailing_ones(); + mask &= !((((1u64 << len) - 1) << start) as u32); + Some((start as i32, len as i32)) + }) +} + +/// Without an indirect count, the slots each region draws: an earlier +/// frame's count plus a quarter and a floor for the temporal region's +/// spikes, within the region. +fn draw_slots(last_counts: &[u32; COUNT_WORDS]) -> [u32; 3] { + Region::ALL.map(|region| { + let (_, count_offset, max_draws) = region.layout(); + let count = last_counts[count_offset as usize / 4]; + count.saturating_add(count / 4 + 256).min(max_draws) + }) +} + +/// `SectionMeta.writeMetadata`: the key high word first, the bounds, the +/// first quad `ptr`; then the eight groups' quad counts in `u16` pairs. +fn section_record(section: &BuiltSection, ptr: u32) -> [[u32; 4]; 2] { + let key = section.key.pack(); + let count = section.quads.len() as u32; + let group_count = |group: usize| { + let end = section.offsets.get(group + 1).copied().unwrap_or(count); + end - section.offsets[group] + }; + let pair = |group: usize| group_count(group) | group_count(group + 1) << 16; + [ + [(key >> 32) as u32, key as u32, section.aabb, ptr], + [pair(0), pair(2), pair(4), pair(6)], + ] +} + +/// Every level of the model atlas. +const MODEL_ATLAS_RANGE: rhi::TextureRange = rhi::TextureRange { + mips: MODEL_MIP_LEVELS, + ..rhi::TextureRange::BASE +}; + +fn atlas_transition( + atlas: rhi::backend::TextureHandle, + from: rhi::TextureUse, + to: rhi::TextureUse, +) -> rhi::backend::TextureTransition { + rhi::backend::TextureTransition { + texture: atlas, + range: MODEL_ATLAS_RANGE, + from, + to, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn runs_split_a_mask_at_its_gaps() { + assert_eq!(runs(0).collect::>(), []); + assert_eq!(runs(0b1).collect::>(), [(0, 1)]); + assert_eq!(runs(0b1110_0000_0110).collect::>(), [(1, 2), (9, 3)]); + assert_eq!(runs(u32::MAX).collect::>(), [(0, 32)]); + assert_eq!(runs(1 << 31 | 1).collect::>(), [(0, 1), (31, 1)]); + } + + #[test] + fn section_records_pack_the_key_bounds_and_count_pairs() { + let key = SectionKey { + level: 2, + x: 5, + y: -1, + z: 9, + }; + let mut section = BuiltSection::empty(key, 0b11); + section.aabb = 0xABCD; + section.quads = vec![0; 10]; + section.offsets = [0, 1, 1, 3, 6, 6, 6, 8]; + let packed = key.pack(); + assert_eq!( + section_record(§ion, 77), + [ + [(packed >> 32) as u32, packed as u32, 0xABCD, 77], + [1, 2 | 3 << 16, 0, 2 | 2 << 16] + ] + ); + } + + #[test] + fn regions_tile_the_draw_list_and_counts_fit_their_buffer() { + let stride = size_of::() as u64; + let mut regions: Vec<(u32, u64, u32)> = Region::ALL.map(Region::layout).to_vec(); + regions.sort_unstable_by_key(|&(first, ..)| first); + let mut next = 0; + for &(first, _, draws) in ®ions { + assert_eq!(first, next); + next += draws; + } + assert_eq!(u64::from(next) * stride, DRAWS_BYTES); + // The counts follow cmdgen's dispatch size, a word each, before the + // occlusion pass's draw. + let mut counts: Vec = regions.iter().map(|&(_, count, _)| count).collect(); + counts.sort_unstable(); + assert_eq!(counts, [12, 16, 20]); + assert_eq!(COUNTS_CULL_DRAW, 24); + assert!(COUNTS_CULL_DRAW + stride <= COUNTS_BYTES); + } + + #[test] + fn draw_slots_grow_on_the_last_counts_within_their_regions() { + let mut counts = [0; COUNT_WORDS]; + assert_eq!(draw_slots(&counts), [256; 3]); + counts[COUNTS_OPAQUE as usize / 4] = 1000; + counts[COUNTS_TEMPORAL as usize / 4] = u32::MAX; + counts[COUNTS_TRANSLUCENT as usize / 4] = TRANSLUCENT_DRAWS; + let slots = draw_slots(&counts); + assert_eq!(slots[Region::Opaque as usize], 1000 + 250 + 256); + assert_eq!(slots[Region::Temporal as usize], TEMPORAL_DRAWS); + assert_eq!(slots[Region::Translucent as usize], TRANSLUCENT_DRAWS); + } + + #[test] + fn cube_faces_wind_outward() { + let corner = |i: u32| [i & 1, i >> 1 & 1, i >> 2].map(|c| c as f32 - 0.5); + let facing: Vec = CUBE + .chunks(3) + .map(|tri| { + let [a, b, c] = [corner(tri[0]), corner(tri[1]), corner(tri[2])]; + let edge = |p: [f32; 3], q: [f32; 3]| [q[0] - p[0], q[1] - p[1], q[2] - p[2]]; + let (u, v) = (edge(a, b), edge(a, c)); + let normal = [ + u[1] * v[2] - u[2] * v[1], + u[2] * v[0] - u[0] * v[2], + u[0] * v[1] - u[1] * v[0], + ]; + // The face's centre lies along its normal when it points out. + (0..3) + .map(|i| normal[i] * (a[i] + b[i] + c[i])) + .sum::() + > 0.0 + }) + .collect(); + assert!(facing.iter().all(|&f| f), "{facing:?}"); + } +} diff --git a/pomme-client/src/renderer/distant/mod.rs b/pomme-client/src/renderer/distant/mod.rs new file mode 100644 index 000000000..a45219168 --- /dev/null +++ b/pomme-client/src/renderer/distant/mod.rs @@ -0,0 +1,19 @@ +//! Distant terrain: every chunk seen, kept as sections at five levels of +//! detail (on disk too, per server or world) and drawn past the loaded +//! ones from models baked per block state, the GPU choosing each frame +//! which sections to draw. A Pomme-only system; the game has +//! nothing like it. +// TODO: generation beyond what's been seen, shader packs, and dimensions +// without sky light (a cave roof would cover them). + +pub(crate) mod bakery; +pub(crate) mod build; +pub(crate) mod colors; +pub(crate) mod gpu; +pub(crate) mod models; +pub(crate) mod nodes; +pub(crate) mod scan; +pub(crate) mod section; +pub(crate) mod voxel; +pub(crate) mod worker; +pub(crate) mod world; diff --git a/pomme-client/src/renderer/distant/models.rs b/pomme-client/src/renderer/distant/models.rs new file mode 100644 index 000000000..fb356da6b --- /dev/null +++ b/pomme-client/src/renderer/distant/models.rs @@ -0,0 +1,826 @@ +//! Distant terrain's models: block states baked into models, each six face +//! textures with mips in a tile of the model atlas, plus what the mesher and +//! shaders need to know of each face. + +use std::collections::HashMap; + +use azalea_block::BlockState; + +use super::bakery::{Bakery, FaceTexture, MODEL_TEXTURE_SIZE, TEXELS}; +use crate::renderer::chunk::atlas::AtlasUVMap; +use crate::renderer::chunk::mesher::int_to_rgb; +use crate::world::block::model::Direction; +use crate::world::block::render_assets::{BlockRenderView, Tint}; +use crate::world::block::{BlockStateData, FluidKind}; + +const SIZE: usize = MODEL_TEXTURE_SIZE; +/// Mip levels of a face texture: 16, 8, 4 and 2 texels. +pub(crate) const MODEL_MIP_LEVELS: u32 = SIZE.trailing_zeros(); +/// A model's tile: its six faces two rows of three, face index `i` at column +/// `i / 2`, row `i % 2`. +pub(crate) const TILE_WIDTH: u32 = SIZE as u32 * 3; +pub(crate) const TILE_HEIGHT: u32 = SIZE as u32 * 2; +/// Tiles a row of the model atlas holds, and rows. +pub(crate) const ATLAS_COLUMNS: u32 = 64; +pub(crate) const ATLAS_ROWS: u32 = 32; +/// Models the atlas holds; the first is air, drawn as nothing. +pub(crate) const CAPACITY: usize = (ATLAS_COLUMNS * ATLAS_ROWS) as usize; + +/// Which layer a model draws in (vanilla `ChunkSectionLayer`). +#[derive(Clone, Copy, PartialEq, Eq)] +enum Layer { + Solid, + Cutout, + Translucent, +} + +/// What the mesher asks of each model: a byte per face, bit 0 the face +/// occludes what it touches, 1 it covers its whole side, 2 it can be +/// occluded, 3 it's lit by its own block, 0xFF for no face; in byte 6 whether +/// the model is biome-coloured, translucent, double-sided, holds a fluid, is +/// a fluid, culls its own kind and is fully opaque; the light it gives off in +/// bits 55-58. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct Metadata(pub u64); + +impl Metadata { + fn face(self, face: u8) -> u64 { + self.0 >> (8 * face) & 0xFF + } + + pub fn face_exists(self, face: u8) -> bool { + self.face(face) != 0xFF + } + + pub fn face_occludes(self, face: u8) -> bool { + self.face_exists(face) && self.face(face) & 1 != 0 + } + + pub fn face_can_be_occluded(self, face: u8) -> bool { + self.face(face) & 4 != 0 + } + + pub fn face_uses_self_lighting(self, face: u8) -> bool { + self.face(face) & 8 != 0 + } + + fn flag(self, bit: u32) -> bool { + self.0 >> 48 & 1 << bit != 0 + } + + pub fn is_biome_coloured(self) -> bool { + self.flag(0) + } + + pub fn is_translucent(self) -> bool { + self.flag(1) + } + + pub fn is_double_sided(self) -> bool { + self.flag(2) + } + + pub fn contains_fluid(self) -> bool { + self.flag(3) + } + + pub fn is_fluid(self) -> bool { + self.flag(4) + } + + /// A fluid, or a block holding one. + pub fn has_fluid(self) -> bool { + self.is_fluid() || self.contains_fluid() + } + + pub fn culls_same(self) -> bool { + self.flag(5) + } + + pub fn is_fully_opaque(self) -> bool { + self.flag(6) + } + + pub fn light_emission(self) -> u64 { + self.0 >> 55 & 0xF + } +} + +/// The models the mesher reads, baked as it asks. +pub(crate) trait Models { + /// Block state `block`'s model, 0 for none. + fn model(&mut self, block: u32) -> u16; + fn metadata(&self, model: u16) -> Metadata; + /// The model of the fluid a model's block holds, for a fluid its own; + /// for a fluid's lowered surface ([`Self::fluid_surface`]) the fluid's. + fn fluid_model(&self, model: u16) -> u16; + /// A fluid's model with its surface `gap` blocks below the top of a + /// level-`level` voxel, inset as at level 0 rather than scaled with the + /// voxel; the fluid's own at level 0. Fluid never faces fluid, so where + /// a neighbouring column's surface sits lower the higher one shows no + /// side between the two heights, inside a section or across a seam. + fn fluid_surface(&mut self, fluid: u16, level: u8, gap: u8) -> u16; +} + +/// What tints a model's texture: a fixed colour, or the biome's (in the +/// order of [`super::colors::TINT_KINDS`]). +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub(crate) enum ModelTint { + None, + Constant([u8; 3]), + Grass, + Foliage, + DryFoliage, + Water, +} + +impl ModelTint { + fn biome_dependent(self) -> bool { + !matches!(self, Self::None | Self::Constant(_)) + } + + /// [`BlockModel::tint`]: the colour, or the kind to look the biome's up + /// by. + fn packed(self) -> u32 { + match self { + Self::None => u32::MAX, + Self::Constant([r, g, b]) => u32::from_le_bytes([r, g, b, 255]), + Self::Grass => 0, + Self::Foliage => 1, + Self::DryFoliage => 2, + Self::Water => 3, + } + } +} + +/// Moves a fluid model's surface `gap` blocks down into a level-`level` +/// voxel, keeping the inset below it the level-0 model's in blocks: the top +/// face's inset, in 64ths of the voxel, and the end of the sides' drawn +/// texels. +fn lower_surface(face_data: &mut [u32; 6], level: u8, gap: u8) { + let blocks = u32::from(gap); + let voxel = 1u32 << level; + let top = &mut face_data[1]; + let inset = (blocks * 64 + (*top >> 16 & 63)) / voxel; + *top = *top & !(63 << 16) | inset.min(62) << 16; + for side in &mut face_data[2..] { + if *side == u32::MAX { + continue; + } + let min_y = *side >> 8 & 0xF; + let max_y = (16 * (voxel - blocks) / voxel).saturating_sub(1).max(min_y); + *side = *side & !(0xF << 12) | max_y << 12; + } +} + +/// A tint colour as the shaders unpack it. +pub(crate) fn pack_tint(rgb: [f32; 3]) -> u32 { + let [r, g, b] = rgb.map(|c| (c * 255.0).round().clamp(0.0, 255.0) as u8); + u32::from_le_bytes([r, g, b, 255]) +} + +/// A model as the shaders read it (`distant_models.glsl`). +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable)] +pub(crate) struct BlockModel { + /// Per face: the drawn texels' min x, max x, min y and max y in 4 bits + /// each, how far in from its side the face sits in 64ths from bit 16, + /// whether it cuts clear texels out (bit 22) and does so once a quad + /// spans more than one block (23), and its tint state in bits 24-25: 1 + /// on some texels, 2 on all. + pub face_data: [u32; 6], + /// Tinted (1), by the biome (2), translucent (4), shaded per face (8, + /// [`super::bakery::BakeFlags::shaded`]). + pub flags: u32, + /// [`ModelTint::packed`]. + pub tint: u32, +} + +/// What a model is deduplicated by (`ModelEntry`): states that look alike +/// share one. +#[derive(PartialEq, Eq, Hash)] +struct ModelEntry { + faces: Box<[FaceTexture; 6]>, + /// The fluid a non-fluid block holds, by model. + fluid: Option, + tint: ModelTint, +} + +/// A state's model is still to bake. +const UNBAKED: u16 = u16::MAX; + +/// What the mesher needs of a model. +#[derive(Clone, Copy)] +pub(crate) struct ModelInfo { + pub metadata: Metadata, + /// [`Models::fluid_model`]. + pub fluid: u16, +} + +/// Model 0's place: air. +const AIR: ModelInfo = ModelInfo { + metadata: Metadata(0), + fluid: 0, +}; + +/// A new model: its record and its tile with mips, to upload. +pub(crate) struct ModelTile { + pub id: u16, + pub model: BlockModel, + /// RGBA per level, the level-0 tile first, row by row. + pub levels: Vec>, +} + +pub(crate) struct ModelFactory { + bakery: Bakery, + /// Each state's model by state id, once baked. + id_mappings: Vec, + /// Models by what they're made of, so states that look alike share one. + entries: HashMap, + models: Vec, + /// Tiles baked since the last [`Self::take_tiles`]. + tiles: Vec, + /// Each fluid's record and tile, for its lowered surfaces to copy. + fluids: HashMap>)>, + /// [`Self::fluid_surface`]'s models by fluid, level and gap. + surfaces: HashMap<(u16, u8, u8), u16>, +} + +impl ModelFactory { + pub fn new() -> Self { + Self { + bakery: Bakery::new(), + id_mappings: Vec::new(), + entries: HashMap::new(), + models: vec![AIR], + tiles: Vec::new(), + fluids: HashMap::new(), + surfaces: HashMap::new(), + } + } + + /// [`Models::fluid_surface`]. + pub fn fluid_surface(&mut self, fluid: u16, level: u8, gap: u8) -> u16 { + if level == 0 { + return fluid; + } + if let Some(&id) = self.surfaces.get(&(fluid, level, gap)) { + return id; + } + let Some((record, levels)) = self.fluids.get(&fluid) else { + return fluid; + }; + if self.models.len() == CAPACITY { + return fluid; + } + let mut model = *record; + lower_surface(&mut model.face_data, level, gap); + let id = self.models.len() as u16; + self.models.push(ModelInfo { + metadata: self.models[usize::from(fluid)].metadata, + fluid, + }); + self.tiles.push(ModelTile { + id, + model, + levels: levels.clone(), + }); + self.surfaces.insert((fluid, level, gap), id); + id + } + + /// Whether voxels of a model tinted `tint`, holding `held_fluid`, keep + /// their biome: for their own tint or for the fluid's. Only the metadata + /// takes the fluid's; the model's own record keeps its tint's, since an + /// untinted block has no biome colours to look up and the fluid's quads + /// draw with the fluid's record. + fn biome_coloured(&self, tint: ModelTint, held_fluid: Option) -> bool { + tint.biome_dependent() + || held_fluid + .is_some_and(|fluid| self.models[usize::from(fluid)].metadata.is_biome_coloured()) + } + + pub fn info(&self, model: u16) -> ModelInfo { + self.models[usize::from(model)] + } + + pub fn take_tiles(&mut self) -> Vec { + std::mem::take(&mut self.tiles) + } + + /// `state`'s model, baked the first time; 0, air, when it draws nothing + /// or the atlas is full. + pub fn model( + &mut self, + view: BlockRenderView<'_>, + uv_map: &AtlasUVMap, + state: BlockState, + ) -> u16 { + let slot = usize::from(state.id()); + if slot >= self.id_mappings.len() { + self.id_mappings.resize(slot + 1, UNBAKED); + } + if self.id_mappings[slot] != UNBAKED { + return self.id_mappings[slot]; + } + let id = self.bake(view, uv_map, state); + self.id_mappings[slot] = id; + id + } + + /// Bakes `state`'s faces, picks the model's layer and works out what the + /// mesher and shaders need of each face; a model that looks like an + /// earlier one is that one. + fn bake(&mut self, view: BlockRenderView<'_>, uv_map: &AtlasUVMap, state: BlockState) -> u16 { + let Some((faces, flags)) = self.bakery.render(view, uv_map, state) else { + return 0; + }; + let table = view.block_state_table(); + let data = table.block_state_data(state); + let tint = match flags.tint { + Tint::None => ModelTint::None, + Tint::Grass | Tint::DoubleGrass => ModelTint::Grass, + Tint::Foliage => ModelTint::Foliage, + Tint::DryFoliage => ModelTint::DryFoliage, + Tint::Water => ModelTint::Water, + Tint::Constant(rgb) => ModelTint::Constant(to_bytes(int_to_rgb(rgb as i32))), + Tint::Redstone => ModelTint::Constant(to_bytes(data.redstone_wire_rgb())), + Tint::Stem => ModelTint::Constant(to_bytes(data.stem_rgb())), + }; + let is_fluid = data.is_fluid_block(); + // A block holding a fluid draws the fluid's model around it. + let held_fluid = match data.fluid().kind { + _ if is_fluid => None, + FluidKind::Empty => Some(0), + _ => Some(self.model(view, uv_map, table.fluid_block(data.fluid()))), + }; + let entry = ModelEntry { + faces: Box::new(faces), + fluid: held_fluid, + tint, + }; + if let Some(&id) = self.entries.get(&entry) { + return id; + } + if self.models.len() == CAPACITY { + // TODO: a model atlas that grows past its capacity. + return 0; + } + let faces = &entry.faces; + let leaves = data.is_leaves(); + let mut layer = None; + if flags.translucent { + layer = Some(if faces.iter().any(has_translucent_pixel) { + Layer::Translucent + } else if faces.iter().all(is_solid_where_drawn) { + Layer::Solid + } else { + Layer::Cutout + }); + } + if layer.is_none() && flags.discard { + layer = Some(Layer::Cutout); + } + if leaves { + layer = Some(Layer::Solid); + } + let layer = layer.unwrap_or(Layer::Solid); + let check = if layer == Layer::Solid { + WriteCheck::Stencil + } else { + WriteCheck::Alpha + }; + let depth_mode = if layer == Layer::Solid { + DepthMode::Avg + } else { + DepthMode::Min + }; + let depths: [Option; 6] = + std::array::from_fn(|i| compute_depth(&faces[i], depth_mode, check)); + // Nothing on either side of an axis: plants and the like, drawn from + // both sides. + let double_sided = [0, 2, 4] + .iter() + .any(|&a| depths[a].is_none() && depths[a + 1].is_none()); + let biome_coloured = self.biome_coloured(tint, held_fluid); + let mut metadata = u64::from(biome_coloured) + | u64::from(layer == Layer::Translucent) << 1 + | u64::from(double_sided) << 2 + | u64::from(!is_fluid && data.fluid().kind != FluidKind::Empty) << 3 + | u64::from(is_fluid) << 4 + | u64::from(culls_same(data)) << 5; + let mut fully_opaque = true; + let mut face_data = [u32::MAX; 6]; + // Highest face first, so each shifts into place. + for face in (0..6).rev() { + metadata <<= 8; + let Some(offset) = depths[face] else { + metadata |= 0xFF; + fully_opaque = false; + continue; + }; + let texture = &faces[face]; + let [min_x, max_x, min_y, max_y] = compute_bounds(texture, check); + let written = written_pixel_count(texture, check) as f32; + let covers_block = min_x == 0 && min_y == 0 && max_x == SIZE - 1 && max_y == SIZE - 1; + metadata |= u64::from(covers_block) << 1; + let occludes = face_occludes(layer, is_fluid, face, offset, written); + metadata |= u64::from(occludes); + fully_opaque &= occludes; + metadata |= u64::from(offset < 0.3) << 2; + metadata |= u64::from(offset > 0.01 || layer == Layer::Translucent) << 3; + let area = ((max_x + 1 - min_x) * (max_y + 1 - min_y)) as f32; + // Fences and the like are solid with gaps between their parts. + let discard = + (written / area < 0.9 || layer != Layer::Solid) && layer != Layer::Translucent; + let discard_when_merged = !covers_block && layer != Layer::Translucent; + let tint_state = match face_tint(texture, check) { + _ if tint == ModelTint::None => 0, + FaceTint::Some => 1, + FaceTint::All => 2, + FaceTint::Nothing | FaceTint::None => 0, + }; + face_data[face] = (min_x | max_x << 4 | min_y << 8 | max_y << 12) as u32 + | ((offset * 64.0).round() as u32).min(62) << 16 + | u32::from(discard) << 22 + | u32::from(discard_when_merged) << 23 + | tint_state << 24; + } + metadata |= u64::from(fully_opaque) << 54; + metadata |= u64::from(data.light_props().emission) << 55; + let id = self.models.len() as u16; + self.models.push(ModelInfo { + metadata: Metadata(metadata), + fluid: held_fluid.unwrap_or(id), + }); + let model = BlockModel { + face_data, + flags: u32::from(tint != ModelTint::None) + | u32::from(tint.biome_dependent()) << 1 + | u32::from(layer == Layer::Translucent) << 2 + | u32::from(flags.shaded) << 3, + tint: tint.packed(), + }; + let levels = mip_tile(flags.darkened, leaves, faces); + if is_fluid { + self.fluids.insert(id, (model, levels.clone())); + } + self.tiles.push(ModelTile { id, model, levels }); + self.entries.insert(entry, id); + id + } +} + +/// Whether a block culls its own kind: only one that does on every side. +fn culls_same(data: &BlockStateData) -> bool { + Direction::VALUES.iter().all(|&dir| data.culls_same(dir)) +} + +/// Whether face `face` (`written` texels drawn, `offset` in from its side) +/// hides the block it touches: not when translucent, drawn far in from its +/// side, or mostly clear. A fluid's top and sides never do: they end at +/// `FluidRenderer`'s 8/9 in the reference, short of occluding, where ours +/// are drawn full height ([`super::bakery`]'s `fluid_quads`). +fn face_occludes(layer: Layer, is_fluid: bool, face: usize, offset: f32, written: f32) -> bool { + layer != Layer::Translucent + && !(is_fluid && face != 0) + && offset < 0.1 + && written / TEXELS as f32 > 0.9 +} + +fn to_bytes(rgb: [f32; 3]) -> [u8; 3] { + pack_tint(rgb).to_le_bytes()[..3] + .try_into() + .expect("three channels") +} + +/// How a drawn texel is told from a clear one. +#[derive(Clone, Copy)] +enum WriteCheck { + /// Some quad covered it. + Stencil, + /// Its alpha isn't (near) zero. + Alpha, +} + +fn was_pixel_written(face: &FaceTexture, check: WriteCheck, i: usize) -> bool { + match check { + WriteCheck::Stencil => face.depth[i] & 0xFF != 0, + // Some come out with an alpha of 1 where they're meant to be 0. + WriteCheck::Alpha => (face.colour[i] >> 24) & 0xFF > 1, + } +} + +fn written_pixel_count(face: &FaceTexture, check: WriteCheck) -> usize { + (0..TEXELS) + .filter(|&i| was_pixel_written(face, check, i)) + .count() +} + +fn has_translucent_pixel(face: &FaceTexture) -> bool { + (0..TEXELS).any(|i| { + let alpha = face.colour[i] >> 24; + face.depth[i] & 0xFF != 0 && alpha != 0 && alpha != 255 + }) +} + +fn is_solid_where_drawn(face: &FaceTexture) -> bool { + (0..TEXELS).all(|i| face.depth[i] & 0xFF == 0 || face.colour[i] >> 24 == 255) +} + +enum FaceTint { + Nothing, + None, + Some, + All, +} + +/// Which of the face's drawn texels are tinted, counting those that aren't +/// black or clear. +fn face_tint(face: &FaceTexture, check: WriteCheck) -> FaceTint { + let (mut all, mut some, mut written) = (true, false, false); + for i in 0..TEXELS { + let colour = face.colour[i]; + if !was_pixel_written(face, check, i) || colour & 0xFF_FFFF == 0 || colour >> 24 == 0 { + continue; + } + let tinted = face.depth[i] & (1 << 7) != 0; + written = true; + all &= tinted; + some |= tinted; + } + match (written, some, all) { + (false, ..) => FaceTint::Nothing, + (true, false, _) => FaceTint::None, + (true, true, false) => FaceTint::Some, + (true, true, true) => FaceTint::All, + } +} + +#[derive(Clone, Copy)] +enum DepthMode { + Avg, + Min, +} + +/// How far in from its side the face's drawn texels sit, 0 to 1; `None` with +/// nothing drawn. +fn compute_depth(face: &FaceTexture, mode: DepthMode, check: WriteCheck) -> Option { + let depths = (0..TEXELS) + .filter(|&i| was_pixel_written(face, check, i)) + .map(|i| u64::from(face.depth[i] >> 8)); + let depth = match mode { + DepthMode::Avg => { + let (count, sum) = depths.fold((0, 0), |(n, sum), d| (n + 1, sum + d)); + (count != 0).then(|| sum / count)? + } + DepthMode::Min => depths.min()?, + }; + Some((depth as f64 / f64::from((1 << 24) - 1)) as f32) +} + +/// The drawn texels' `[min_x, max_x, min_y, max_y]`. +fn compute_bounds(face: &FaceTexture, check: WriteCheck) -> [usize; 4] { + let written = |x: usize, y: usize| was_pixel_written(face, check, x + y * SIZE); + let column = |x| (0..SIZE).any(|y| written(x, y)); + let row = |y| (0..SIZE).any(|x| written(x, y)); + let min_x = (0..SIZE).find(|&x| column(x)).unwrap_or(SIZE); + let max_x = (0..SIZE).rev().find(|&x| column(x)).unwrap_or(0); + let min_y = (0..SIZE).find(|&y| row(y)).unwrap_or(SIZE); + let max_y = (0..SIZE).rev().find(|&y| row(y)).unwrap_or(0); + [min_x, max_x, min_y, max_y] +} + +/// The faces laid out in a tile, transparent texels taking the colour of the +/// nearest drawn one, then halved down to 2x2 faces. Sprites that mip dark +/// keep their transparent texels black, as `dark_cutout` mips them; leaves +/// fill theirs anyway, since coarse distant leaves draw them solid. +fn mip_tile(darkened: bool, leaves: bool, faces: &[FaceTexture; 6]) -> Vec> { + let width = TILE_WIDTH as usize; + let mut tile = vec![0u32; width * TILE_HEIGHT as usize]; + for (i, face) in faces.iter().enumerate() { + let (x, y) = ((i >> 1) * SIZE, (i & 1) * SIZE); + let mut colours = *face.colour; + if (!darkened || leaves) && colours.iter().any(|c| c >> 24 == 0) { + solidify(&mut colours); + } + for (j, &colour) in colours.iter().enumerate() { + tile[(y + j / SIZE) * width + x + j % SIZE] = colour; + } + } + let mut levels = vec![tile]; + for level in 1..MODEL_MIP_LEVELS as usize { + let source = &levels[level - 1]; + let source_width = width >> (level - 1); + let (w, h) = (width >> level, TILE_HEIGHT as usize >> level); + let next = (0..w * h) + .map(|i| { + let at = (i % w) * 2 + (i / w) * 2 * source_width; + mip_colours( + darkened, + [ + source[at], + source[at + source_width], + source[at + 1], + source[at + source_width + 1], + ], + ) + }) + .collect(); + levels.push(next); + } + levels + .into_iter() + .map(|level| level.iter().flat_map(|c| c.to_le_bytes()).collect()) + .collect() +} + +/// A breadth-first walk out from the drawn texels, each clear one taking the +/// nearest one's colour and keeping its own zero alpha, so mips don't bleed +/// black. +fn solidify(colours: &mut [u32; TEXELS]) { + // Per texel, the distance walked (high byte) and the drawn texel reached. + let mut nearest = [u16::MAX; TEXELS]; + let mut queue = std::collections::VecDeque::new(); + for (i, colour) in colours.iter().enumerate() { + if colour & 0xFF00_0000 != 0 { + nearest[i] = i as u16; + queue.push_back(i); + } + } + while let Some(at) = queue.pop_front() { + let (x, y) = ((at % SIZE) as i32, (at / SIZE) as i32); + let next = nearest[at] + 0x0100; + for (dx, dy) in [(0, 1), (0, -1), (1, 0), (-1, 0)] { + let (x2, y2) = (x + dx, y + dy); + if !(0..SIZE as i32).contains(&x2) || !(0..SIZE as i32).contains(&y2) { + continue; + } + let to = x2 as usize + y2 as usize * SIZE; + if next & 0xFF00 < nearest[to] & 0xFF00 { + nearest[to] = next; + queue.push_back(to); + } + } + } + for i in 0..TEXELS { + if nearest[i] & 0xFF00 != 0 { + colours[i] = colours[usize::from(nearest[i] & 0xFF)] & 0x00FF_FFFF; + } + } +} + +/// The mean of four ABGR texels in linear space. Dark-mipped sprites count +/// only texels with alpha and still divide by four, darkening their edges as +/// vanilla's `dark_cutout` does. +fn mip_colours(darkened: bool, texels: [u32; 4]) -> u32 { + let mut sum = [0.0f32; 4]; + for texel in texels { + if darkened && texel >> 24 == 0 { + continue; + } + for (channel, total) in sum.iter_mut().enumerate().take(3) { + *total += srgb_to_linear((texel >> (channel * 8)) as u8); + } + let alpha = (texel >> 24) as u8; + sum[3] += if darkened { + srgb_to_linear(alpha) + } else { + f32::from(alpha) + }; + } + let [r, g, b, a] = sum.map(|total| total / 4.0); + let alpha = if darkened { + linear_to_srgb(a) + } else { + (sum[3] as u32 / 4) as u8 + }; + u32::from_le_bytes([ + linear_to_srgb(r), + linear_to_srgb(g), + linear_to_srgb(b), + alpha, + ]) +} + +fn srgb_to_linear(channel: u8) -> f32 { + let c = f32::from(channel) / 255.0; + if c <= 0.04045 { + c / 12.92 + } else { + ((c + 0.055) / 1.055).powf(2.4) + } +} + +fn linear_to_srgb(linear: f32) -> u8 { + let c = if linear <= 0.003_130_8 { + linear * 12.92 + } else { + 1.055 * linear.powf(1.0 / 2.4) - 0.055 + }; + (c * 255.0).round().clamp(0.0, 255.0) as u8 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn lowering_a_surface_moves_the_top_and_cuts_the_sides() { + // A fluid: top inset two texels, sides the full voxel. + let water = [u32::MAX, 8 << 16, 0xF0F0, 0xF0F0, 0xF0F0, 0xF0F0]; + let mut faces = water; + lower_surface(&mut faces, 1, 1); + // A block down a two-block voxel: (64 + 8) / 2 sixty-fourths. + assert_eq!(faces[1] >> 16 & 63, 36); + // The sides end at the lower half, texel row 7. + assert_eq!(faces[2] >> 12 & 0xF, 7); + assert_eq!(faces[0], u32::MAX); + let mut faces = water; + lower_surface(&mut faces, 4, 15); + assert_eq!(faces[1] >> 16 & 63, 60); + assert_eq!(faces[2] >> 12 & 0xF, 0); + // At the top of a two-block voxel the inset halves and the sides stay. + let mut faces = water; + lower_surface(&mut faces, 1, 0); + assert_eq!(faces[1] >> 16 & 63, 4); + assert_eq!(faces[2] >> 12 & 0xF, 15); + } + + #[test] + fn a_block_holding_water_keeps_the_biome_for_it() { + let mut factory = ModelFactory::new(); + factory.models.push(ModelInfo { + metadata: Metadata(1 << 48 | 16 << 48), + fluid: 1, + }); + assert!(factory.biome_coloured(ModelTint::None, Some(1))); + assert!(!factory.biome_coloured(ModelTint::None, Some(0))); + assert!(!factory.biome_coloured(ModelTint::Constant([1, 2, 3]), None)); + assert!(factory.biome_coloured(ModelTint::Foliage, Some(0))); + } + + #[test] + fn a_fluid_occludes_only_below_so_is_never_fully_opaque() { + let full = TEXELS as f32; + let occluding: Vec = (0..6) + .filter(|&face| face_occludes(Layer::Solid, true, face, 0.0, full)) + .collect(); + assert_eq!(occluding, [0]); + assert!((0..6).all(|face| face_occludes(Layer::Solid, false, face, 0.0, full))); + } + + #[test] + fn only_blocks_culling_their_kind_on_every_side_cull_same() { + let table = crate::world::block::BlockStateTable::native(); + let culls = |name| culls_same(table.block_state_data(table.first_state_of(name).unwrap())); + assert!(culls("glass") && culls("water")); + // Mangrove roots skip their own kind only above and below. + assert!(!culls("mangrove_roots") && !culls("stone")); + } + + /// Six faces of green with a clear texel at the corner of each. + fn holey_faces() -> [FaceTexture; 6] { + std::array::from_fn(|_| { + let mut colour = Box::new([u32::from_le_bytes([0, 200, 0, 255]); TEXELS]); + colour[0] = 0; + FaceTexture { + colour, + depth: Box::new([1; TEXELS]), + } + }) + } + + #[test] + fn leaves_fill_their_holes_where_dark_sprites_stay_black() { + // The clear texel takes its neighbour's colour, keeping zero alpha so + // full detail still cuts it out. + let filled = [0, 200, 0, 0]; + for (darkened, leaves, corner) in [ + (true, true, filled), + (true, false, [0; 4]), + (false, false, filled), + ] { + let levels = mip_tile(darkened, leaves, &holey_faces()); + assert_eq!( + levels[0][..4], + corner, + "darkened {darkened}, leaves {leaves}" + ); + } + } + + #[test] + fn metadata_fields_unpack() { + let faces: u64 = (0..6).fold(0, |m, f| m | 0b1011 << (8 * f)) & !(0xFF << 16) | 0xFF << 16; + let meta = Metadata(faces | (2 | 32 | 64) << 48 | 7 << 55); + assert!(meta.face_exists(0) && !meta.face_exists(2)); + assert!(meta.face_occludes(1) && !meta.face_occludes(2)); + assert!(!meta.face_can_be_occluded(0) && meta.face_uses_self_lighting(0)); + assert!(meta.is_translucent() && meta.culls_same() && meta.is_fully_opaque()); + assert!(!meta.is_fluid() && !meta.contains_fluid() && !meta.is_double_sided()); + assert_eq!(meta.light_emission(), 7); + assert_eq!(ModelTint::Constant([1, 2, 3]).packed(), 0xFF03_0201); + assert_eq!(pack_tint([1.0, 0.0, 0.5]), 0xFF80_00FF); + } +} diff --git a/pomme-client/src/renderer/distant/nodes.rs b/pomme-client/src/renderer/distant/nodes.rs new file mode 100644 index 000000000..8f8c3919a --- /dev/null +++ b/pomme-client/src/renderer/distant/nodes.rs @@ -0,0 +1,1469 @@ +//! The node graph: the tree the GPU traversal walks, kept on the CPU and +//! mirrored into the node buffer a record at a time. Every key is one of: a +//! leaf node (a mesh, no children yet), an inner node (children in a +//! consecutive block of ids), or a request waiting on the worker (a +//! top-level node's own mesh, or a node's children). The traversal asks for +//! nodes by key; the worker's meshes answer. + +use std::collections::{HashMap, HashSet}; + +use super::build::BuiltSection; +use super::gpu::consts::{ + CLEANER_SENTINEL, EMPTY_ID as EMPTY, MAX_NODES, NULL_ID as NULL, VISIBILITY_RESET_BIT, +}; +use super::section::SectionKey; + +pub(crate) const NODE_BYTES: u64 = MAX_NODES as u64 * 16; +/// Ids handed out lie below the cleaner's sentinel, so no node is it. +const NODE_IDS: u32 = CLEANER_SENTINEL; + +/// A node's geometry: none yet, nothing to draw, or a section id in the +/// geometry store. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum Mesh { + Null, + Empty, + Id(u32), +} + +impl Mesh { + fn packed(self) -> u32 { + match self { + Self::Null => NULL, + Self::Empty => EMPTY, + Self::Id(id) => id, + } + } +} + +/// Where a node's children are: not made, none at all (an inner node whose +/// existence went to zero), or a block of consecutive ids. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Children { + None, + Empty, + At(u32), +} + +impl Children { + fn packed(self) -> u32 { + match self { + Self::None => NULL, + Self::Empty => EMPTY, + Self::At(ptr) => ptr, + } + } +} + +struct Node { + key: SectionKey, + mesh: Mesh, + child_existence: u8, + children: Children, + /// Ids in the children block, 1 to 8 (meaningless without a block). + child_count: u8, + all_children_leaf: bool, + /// A child request in flight. + request: Option, + /// Its own mesh asked of the worker (an inner node that lost it). + geometry_in_flight: bool, +} + +/// What a key is in the graph. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Entry { + Leaf(u32), + Inner(u32), + Single(usize), + Child(usize), +} + +/// A top-level node waiting for its own mesh and child existence. +struct SingleRequest { + key: SectionKey, + mesh: Option, + child_existence: Option, +} + +impl SingleRequest { + fn satisfied(&self) -> bool { + self.mesh.is_some() && self.child_existence.is_some() + } +} + +/// A node waiting for its children's meshes and their child existence. +struct ChildRequest { + key: SectionKey, + /// Which children it waits on. + mask: u8, + /// Which have a mesh. + results: u8, + /// Which have their child existence. + existence_known: u8, + meshes: [Mesh; 8], + existence: [u8; 8], +} + +impl ChildRequest { + fn new(key: SectionKey) -> Self { + Self { + key, + mask: 0, + results: 0, + existence_known: 0, + meshes: [Mesh::Null; 8], + existence: [0; 8], + } + } + + fn satisfied(&self) -> bool { + self.results & self.mask == self.mask && self.existence_known & self.mask == self.mask + } + + fn require(&mut self, child: usize) { + self.mask |= 1 << child; + } + + fn set_existence(&mut self, child: usize, existence: u8) { + self.existence[child] = existence; + self.existence_known |= 1 << child; + } + + /// Drops `child` from the request, giving back its mesh. + fn remove(&mut self, child: usize) -> Mesh { + let bit = 1 << child; + self.mask &= !bit; + self.results &= !bit; + self.existence_known &= !bit; + std::mem::replace(&mut self.meshes[child], Mesh::Null) + } +} + +/// The node ids in use: allocated one or several consecutive at a time and +/// freed one at a time. +struct IdSet { + words: Vec, + capacity: u32, + /// No free id lies below this word. + first_free: usize, + used: u32, +} + +impl IdSet { + fn new(capacity: u32) -> Self { + Self { + words: vec![0; (capacity as usize).div_ceil(64)], + capacity, + first_free: 0, + used: 0, + } + } + + fn is_set(&self, id: u32) -> bool { + self.words[id as usize / 64] & 1 << (id % 64) != 0 + } + + /// The first run of `count` free ids, taken. + fn alloc(&mut self, count: u32) -> Option { + let mut id = self.first_free as u32 * 64; + while id + count <= self.capacity { + match (id..id + count).find(|&i| self.is_set(i)) { + Some(taken) => id = taken + 1, + None => { + for i in id..id + count { + self.words[i as usize / 64] |= 1 << (i % 64); + } + self.used += count; + while self.words.get(self.first_free) == Some(&u64::MAX) { + self.first_free += 1; + } + return Some(id); + } + } + } + None + } + + fn free(&mut self, id: u32) { + debug_assert!(self.is_set(id), "node {id} is not allocated"); + self.words[id as usize / 64] &= !(1 << (id % 64)); + self.first_free = self.first_free.min(id as usize / 64); + self.used -= 1; + } +} + +/// Requests by id, with freed ids reused. +struct Requests { + slots: Vec>, + free: Vec, +} + +impl Requests { + fn new() -> Self { + Self { + slots: Vec::new(), + free: Vec::new(), + } + } + + fn put(&mut self, request: T) -> usize { + if let Some(id) = self.free.pop() { + self.slots[id] = Some(request); + id + } else { + self.slots.push(Some(request)); + self.slots.len() - 1 + } + } + + fn get(&mut self, id: usize) -> &mut T { + self.slots[id].as_mut().expect("a live request") + } + + fn release(&mut self, id: usize) -> T { + self.free.push(id); + self.slots[id].take().expect("a live request") + } +} + +/// A freed node id: the node whose record pointed at it, none for a +/// top-level node, and its level. +struct Retired { + parent: Option, + level: u8, +} + +pub(crate) struct NodeGraph { + nodes: Vec>, + ids: IdSet, + entries: HashMap, + singles: Requests, + children: Requests, + top_keys: HashSet, + /// The top-level nodes' ids, the traversal's first iteration; a removed + /// id takes the last one's place. + top_ids: Vec, + top_index: HashMap, + /// Nodes whose record changed since [`Self::take_dirty`], by the mark + /// ([`Self::dirty_mark`]) a live one changed at first, or a freed one + /// was freed at. + dirty: HashMap, + /// Bumped by each [`Self::take_dirty`]. + marks: u64, + /// Nodes placed whose record [`Self::take_dirty`] hasn't given yet; the + /// GPU still holds whatever their ids held before. + unwritten: HashSet, + /// Freed ids whose all-ones record [`Self::take_dirty`] hasn't given yet, + /// kept from the free ids till then. + retired: HashMap, + /// Ids allocated ([`VISIBILITY_RESET_BIT`]) or freed, for the cleaner's + /// last-drawn frames. + pub visibility_sets: HashSet, + /// Keys the worker should mesh and keep meshed, and those it should + /// stop; section ids no node holds any more. + pub watch: Vec, + pub unwatch: Vec, + pub freed: Vec, +} + +impl NodeGraph { + pub fn new() -> Self { + Self { + nodes: Vec::new(), + ids: IdSet::new(NODE_IDS), + entries: HashMap::new(), + singles: Requests::new(), + children: Requests::new(), + top_keys: HashSet::new(), + top_ids: Vec::new(), + top_index: HashMap::new(), + dirty: HashMap::new(), + // From 1, so `written_through`'s `first - 1` can't underflow. + marks: 1, + unwritten: HashSet::new(), + retired: HashMap::new(), + visibility_sets: HashSet::new(), + watch: Vec::new(), + unwatch: Vec::new(), + freed: Vec::new(), + } + } + + pub fn top_ids(&self) -> &[u32] { + &self.top_ids + } + + /// The top-level ids whose records the GPU has. + pub fn written_top_ids(&self) -> Vec { + let written = |id: &&u32| !self.unwritten.contains(id); + self.top_ids.iter().filter(written).copied().collect() + } + + /// The mark records change at now: what the GPU's records may name now, + /// they will no longer once [`Self::written_through`] reaches it. + pub fn dirty_mark(&self) -> u64 { + self.marks + } + + /// Every live record changed at or before this mark has been given out. + /// Freed records don't count: the GPU reaches a freed id's old record + /// only through its parent's old record, whose rewrite is a live change + /// made with the free, or through the top-level ids, rewritten each + /// frame. + pub fn written_through(&self) -> u64 { + self.dirty + .iter() + .filter(|&(&id, _)| self.nodes[id as usize].is_some()) + .map(|(_, &mark)| mark) + .min() + .map_or(self.marks, |first| first - 1) + } + + /// Whether `id` holds the node at `key`. + pub fn holds(&self, id: u32, key: SectionKey) -> bool { + self.nodes + .get(id as usize) + .and_then(Option::as_ref) + .is_some_and(|node| node.key == key) + } + + /// The id every node lies below. + pub fn end_id(&self) -> u32 { + self.nodes.len() as u32 + } + + /// Meshes still owed by the worker, for the traversal's request cap. A + /// top-level node's empty request, open until it gets children, owes + /// none. + pub fn in_flight(&self) -> usize { + let singles = self.singles.slots.iter().flatten(); + let children = self.children.slots.iter().flatten(); + singles.filter(|request| request.mesh.is_none()).count() + + children + .map(|request| (request.mask & !request.results).count_ones() as usize) + .sum::() + } + + /// Up to `limit` of the records to write to the node buffer, by id, + /// since last time, such that no written record points at one the GPU + /// lacks; the rest wait for a later call. A node whose mesh is `pending` + /// (its section record not yet written) waits, and so does a parent + /// until its children's records go, so the finest levels go first. A + /// freed id waits for its old parent's record, changed with the free + /// (a top-level one for nothing: the top-level ids are rewritten each + /// frame), then goes back to the free ids. + pub fn take_dirty( + &mut self, + limit: usize, + pending: impl Fn(u32) -> bool, + ) -> Vec<(u32, [u32; 4])> { + let level = |id: u32| self.nodes[id as usize].as_ref().map(|node| node.key.level); + let (mut live, mut freed): (Vec, Vec) = + self.dirty.keys().partition(|&&id| level(id).is_some()); + live.sort_unstable_by_key(|&id| level(id)); + let mut taken = HashSet::new(); + for id in live { + if taken.len() == limit { + break; + } + let node = self.node(id); + let mesh_pending = matches!(node.mesh, Mesh::Id(mesh) if pending(mesh)); + let children_waiting = match node.children { + Children::At(ptr) => (ptr..ptr + u32::from(node.child_count)) + .any(|child| self.dirty.contains_key(&child) && !taken.contains(&child)), + Children::None | Children::Empty => false, + }; + if !mesh_pending && !children_waiting { + taken.insert(id); + } + } + // Coarsest first, so a freed parent goes before its freed children. + freed.sort_unstable_by_key(|id| std::cmp::Reverse(self.retired[id].level)); + for id in freed { + if taken.len() == limit { + break; + } + // Its old parent's record, changed with the free, not yet out. + let parent_waits = self.retired[&id].parent.is_some_and(|parent| { + !taken.contains(&parent) + && self + .dirty + .get(&parent) + .is_some_and(|&mark| mark <= self.dirty[&id]) + }); + if !parent_waits { + taken.insert(id); + } + } + let mut records = Vec::with_capacity(taken.len()); + for id in taken { + self.dirty.remove(&id); + self.unwritten.remove(&id); + if self.retired.remove(&id).is_some() { + self.ids.free(id); + } + records.push((id, self.record(id))); + } + self.marks += 1; + records + } + + /// A node's record: the key, the mesh under the flags' low byte (bit 0 a + /// request in flight), the first child under the high byte (bits 2-4 the + /// children less one, bit 5 all children leaves). A freed id is all + /// ones. + fn record(&self, id: u32) -> [u32; 4] { + let Some(node) = self.nodes[id as usize].as_ref() else { + return [u32::MAX; 4]; + }; + let key = node.key.pack(); + let flags = u32::from(node.request.is_some()) + | u32::from(node.child_count.max(1) - 1) << 2 + | u32::from(node.all_children_leaf) << 5; + [ + (key >> 32) as u32, + key as u32, + node.mesh.packed() | (flags & 0xFF) << 24, + node.children.packed() | (flags >> 8) << 24, + ] + } + + fn node(&self, id: u32) -> &Node { + self.nodes[id as usize].as_ref().expect("a live node") + } + + fn node_mut(&mut self, id: u32) -> &mut Node { + self.nodes[id as usize].as_mut().expect("a live node") + } + + fn invalidate(&mut self, id: u32) { + self.dirty.entry(id).or_insert(self.marks); + } + + /// `id` no longer holds a node: its all-ones record is written, and the + /// id reused, only after its old parent's record, changed with it. + fn retire(&mut self, id: u32, parent: Option, level: u8) { + self.nodes[id as usize] = None; + self.unwritten.remove(&id); + self.track(id, false); + self.dirty.insert(id, self.marks); + self.retired.insert(id, Retired { parent, level }); + } + + /// `id` was allocated or freed, so the cleaner's last-drawn frame for it + /// is reset or cleared; an allocation undoes a free of the same id. + fn track(&mut self, id: u32, allocated: bool) { + let reset = id | VISIBILITY_RESET_BIT; + let (insert, remove) = if allocated { (reset, id) } else { (id, reset) }; + self.visibility_sets.remove(&remove); + self.visibility_sets.insert(insert); + } + + /// `count` consecutive ids, cleared. + fn allocate(&mut self, count: u32) -> u32 { + let base = self + .ids + .alloc(count) + .expect("the node buffer has room for every section in range"); + let end = (base + count) as usize; + if self.nodes.len() < end { + self.nodes.resize_with(end, || None); + } + base + } + + fn place(&mut self, id: u32, key: SectionKey, mesh: Mesh, child_existence: u8) { + self.nodes[id as usize] = Some(Node { + key, + mesh, + child_existence, + children: Children::None, + child_count: 1, + all_children_leaf: false, + request: None, + geometry_in_flight: false, + }); + self.track(id, true); + self.invalidate(id); + self.unwritten.insert(id); + } + + fn free_mesh(&mut self, mesh: Mesh) { + if let Mesh::Id(id) = mesh { + self.freed.push(id); + } + } + + /// Forgets everything, as on a dimension change. + pub fn clear(&mut self) { + let roots: Vec = self.top_keys.iter().copied().collect(); + for key in roots { + self.remove_top_level(key); + } + debug_assert!(self.entries.is_empty()); + } + + /// The top-level nodes become `roots`: gone ones are removed with their + /// trees, new ones requested. + pub fn set_roots(&mut self, roots: &HashSet) { + let gone: Vec = self.top_keys.difference(roots).copied().collect(); + for key in gone { + self.remove_top_level(key); + } + let new: Vec = roots.difference(&self.top_keys).copied().collect(); + for key in new { + self.insert_top_level(key); + } + } + + fn insert_top_level(&mut self, key: SectionKey) { + debug_assert!(!self.entries.contains_key(&key)); + let id = self.singles.put(SingleRequest { + key, + mesh: None, + child_existence: None, + }); + self.watch.push(key); + self.entries.insert(key, Entry::Single(id)); + self.top_keys.insert(key); + } + + fn remove_top_level(&mut self, key: SectionKey) { + self.top_keys.remove(&key); + if let Some(Entry::Leaf(id) | Entry::Inner(id)) = self.entries.get(&key) { + let id = *id; + let index = self.top_index.remove(&id).expect("a top-level id"); + self.top_ids.swap_remove(index); + if let Some(&moved) = self.top_ids.get(index) { + self.top_index.insert(moved, index); + } + } + self.remove_recursive(key, None, false); + } + + /// The worker meshed `key`; `upload` places the quads and gives their + /// section id, or `None` for none, when the store is full. + pub fn process_geometry_result( + &mut self, + section: BuiltSection, + upload: &mut impl FnMut(BuiltSection) -> Option, + ) { + let key = section.key; + let Some(&entry) = self.entries.get(&key) else { + return; + }; + let child_existence = section.child_existence; + match entry { + Entry::Single(id) => { + let old = self.singles.get(id).mesh.unwrap_or(Mesh::Null); + let mesh = self.upload_replace(old, section, upload); + let request = self.singles.get(id); + request.mesh = Some(mesh); + // The result carries the child existence; the watcher's + // separate child-change event has no counterpart here. + request.child_existence.get_or_insert(child_existence); + self.finish_single_if_done(id); + } + Entry::Child(id) => { + let child = child_index(key); + let old = self.children.get(id).meshes[child]; + let mesh = self.upload_replace(old, section, upload); + let request = self.children.get(id); + request.meshes[child] = mesh; + request.results |= 1 << child; + if request.existence_known & 1 << child == 0 { + request.set_existence(child, child_existence); + } + self.finish_child_if_done(id); + } + Entry::Leaf(id) | Entry::Inner(id) => { + if self.node(id).child_existence != child_existence { + self.process_child_change(key, child_existence); + } + let old = self.node(id).mesh; + let mesh = self.upload_replace(old, section, upload); + let node = self.node_mut(id); + node.geometry_in_flight = false; + if node.mesh != mesh { + node.mesh = mesh; + self.invalidate(id); + } + } + } + } + + /// `old` goes, the new quads come, an empty section taking no room. + fn upload_replace( + &mut self, + old: Mesh, + section: BuiltSection, + upload: &mut impl FnMut(BuiltSection) -> Option, + ) -> Mesh { + self.free_mesh(old); + if section.quads.is_empty() { + return Mesh::Empty; + } + // Out of ids, the mesh is dropped until a later build. + upload(section).map_or(Mesh::Empty, Mesh::Id) + } + + fn process_child_change(&mut self, key: SectionKey, child_existence: u8) { + let Some(&entry) = self.entries.get(&key) else { + return; + }; + match entry { + Entry::Single(id) => { + self.singles.get(id).child_existence = Some(child_existence); + self.finish_single_if_done(id); + } + Entry::Child(id) => { + self.children + .get(id) + .set_existence(child_index(key), child_existence); + self.finish_child_if_done(id); + } + Entry::Inner(id) => self.update_children_inner(key, id, child_existence), + Entry::Leaf(id) => { + if let Some(request_id) = self.node(id).request { + // The request follows the new existence: children gone + // leave it, new ones join. + let old_mask = self.children.get(request_id).mask; + let change = old_mask ^ child_existence; + for child in set_bits(change & old_mask) { + self.drop_requested_child(request_id, key, child); + } + for child in set_bits(change & child_existence) { + self.add_requested_child(request_id, key, child); + } + self.finish_child_if_done(request_id); + } + self.node_mut(id).child_existence = child_existence; + self.invalidate(id); + } + } + } + + /// `child` of `parent` leaves request `request_id`: its mesh, entry + /// and watch go. + fn drop_requested_child(&mut self, request_id: usize, parent: SectionKey, child: usize) { + let mesh = self.children.get(request_id).remove(child); + self.free_mesh(mesh); + let key = child_key(parent, child); + let removed = self.entries.remove(&key); + debug_assert_eq!(removed, Some(Entry::Child(request_id))); + self.unwatch.push(key); + } + + fn add_requested_child(&mut self, request_id: usize, parent: SectionKey, child: usize) { + self.children.get(request_id).require(child); + let key = child_key(parent, child); + let previous = self.entries.insert(key, Entry::Child(request_id)); + debug_assert!(previous.is_none(), "{key:?} was already in the graph"); + self.watch.push(key); + } + + /// An inner node's children changed. New ones are asked for; gone ones + /// are removed and the block compacted. + fn update_children_inner(&mut self, key: SectionKey, id: u32, child_existence: u8) { + let existence = self.node(id).child_existence; + let add = (existence ^ child_existence) & child_existence; + if add != 0 { + let request_id = match self.node(id).request { + Some(request_id) => request_id, + None => { + let request_id = self.children.put(ChildRequest::new(key)); + self.node_mut(id).request = Some(request_id); + request_id + } + }; + for child in set_bits(add) { + self.add_requested_child(request_id, key, child); + } + } + // Before the removals, whose finishing expects the new mask. + self.node_mut(id).child_existence = child_existence; + + let mut rem = (existence ^ child_existence) & existence; + if rem != 0 { + if let Some(request_id) = self.node(id).request { + let in_request = self.children.get(request_id).mask & rem; + for child in set_bits(in_request) { + self.drop_requested_child(request_id, key, child); + } + rem ^= in_request; + } + if rem != 0 { + let Children::At(old_ptr) = self.node(id).children else { + unreachable!("an inner node with children to remove has a block"); + }; + let old_count = u32::from(self.node(id).child_count); + let old_existence = self.block_existence(old_ptr, old_count); + debug_assert_eq!(rem & old_existence, rem); + let remaining = rem ^ old_existence; + if remaining == 0 { + let node = self.node_mut(id); + node.all_children_leaf = false; + node.children = Children::Empty; + node.child_count = 8; + for child in set_bits(rem) { + self.remove_recursive(child_key(key, child), Some(id), false); + } + } else { + let new_count = remaining.count_ones(); + let new_ptr = self.allocate(new_count); + let mut next = new_ptr; + let mut all_leaf = true; + let kept = set_bits(old_existence); + for (previous, child) in (old_ptr..).zip(kept) { + if rem & 1 << child != 0 { + self.remove_recursive(child_key(key, child), Some(id), false); + } else { + all_leaf &= self.move_node(previous, next, id); + next += 1; + } + } + let node = self.node_mut(id); + node.all_children_leaf = all_leaf; + node.children = Children::At(new_ptr); + node.child_count = new_count as u8; + } + self.invalidate(id); + } + if let Some(request_id) = self.node(id).request { + self.finish_child_if_done(request_id); + } + } + + if child_existence == 0 { + // No children: back to a leaf, which needs a mesh of its own. + debug_assert!(self.node(id).request.is_none()); + if self.node(id).mesh == Mesh::Null { + if !self.node(id).geometry_in_flight { + self.process_request(key); + } + self.node_mut(id).mesh = Mesh::Empty; + } + debug_assert_eq!(self.node(id).children, Children::Empty); + let node = self.node_mut(id); + node.children = Children::None; + node.all_children_leaf = false; + self.entries.insert(key, Entry::Leaf(id)); + self.invalidate(id); + } + } + + /// Which children the block at `ptr` holds, by child index. + fn block_existence(&self, ptr: u32, count: u32) -> u8 { + (ptr..ptr + count) + .map(|id| 1 << child_index(self.node(id).key)) + .fold(0, |mask, bit| mask | bit) + } + + /// Moves the node at `from` to the free id `to`, keeping its entry; + /// whether it is a leaf. + fn move_node(&mut self, from: u32, to: u32, parent: u32) -> bool { + // Only children move, so `to` needn't join `unwritten`, which keeps + // top-level ids out of the traversal. + debug_assert!( + !self.top_index.contains_key(&from), + "a top-level node moved" + ); + let node = self.nodes[from as usize].take().expect("a live node"); + let key = node.key; + self.nodes[to as usize] = Some(node); + self.retire(from, Some(parent), key.level); + self.track(to, true); + let entry = self.entries.get_mut(&key).expect("a node's entry"); + let (Entry::Leaf(id) | Entry::Inner(id)) = entry else { + unreachable!("a node's entry is a node"); + }; + *id = to; + let leaf = matches!(entry, Entry::Leaf(_)); + self.invalidate(to); + leaf + } + + /// Removes `key` and everything under it; with `only_children`, the node + /// itself stays as a childless leaf. `parent` is the node whose record + /// points at `key`'s, none for a top-level node. + fn remove_recursive(&mut self, key: SectionKey, parent: Option, only_children: bool) { + let entry = if only_children { + self.entries.get(&key).copied() + } else { + self.entries.remove(&key) + }; + let Some(entry) = entry else { + unreachable!("{key:?} is not in the graph"); + }; + match entry { + Entry::Leaf(id) | Entry::Inner(id) => { + let mut child_existence = self.node(id).child_existence; + if let Some(request_id) = self.node(id).request { + child_existence ^= self.children.get(request_id).mask; + self.remove_request(request_id, key); + if only_children { + self.node_mut(id).request = None; + } + } + if let Entry::Inner(_) = entry { + for child in set_bits(child_existence) { + self.remove_recursive(child_key(key, child), Some(id), false); + } + if let Children::At(ptr) = self.node(id).children { + debug_assert!( + (ptr..ptr + u32::from(self.node(id).child_count)) + .all(|child| self.nodes[child as usize].is_none()) + ); + } + if only_children { + self.node_mut(id).children = Children::None; + } + } + if only_children { + self.node_mut(id).all_children_leaf = false; + self.invalidate(id); + } else { + let mesh = self.node(id).mesh; + self.free_mesh(mesh); + self.retire(id, parent, key.level); + self.unwatch.push(key); + } + } + Entry::Single(id) => { + self.unwatch.push(key); + let request = self.singles.release(id); + if let Some(mesh) = request.mesh { + self.free_mesh(mesh); + } + } + Entry::Child(id) => { + self.unwatch.push(key); + self.remove_request(id, key); + } + } + } + + /// Every child request `request_id` holds goes with its mesh, entry and + /// watch. + fn remove_request(&mut self, request_id: usize, parent: SectionKey) { + let mask = self.children.get(request_id).mask; + for child in set_bits(mask) { + self.drop_requested_child(request_id, parent, child); + } + self.children.release(request_id); + } + + fn finish_single_if_done(&mut self, id: usize) { + if self.singles.get(id).satisfied() { + let request = self.singles.release(id); + self.finish_single(request); + } + } + + fn finish_child_if_done(&mut self, id: usize) { + if self.children.get(id).satisfied() { + self.finish_child(id); + } + } + + /// A top-level node's request came back: it becomes a leaf. + fn finish_single(&mut self, request: SingleRequest) { + let id = self.allocate(1); + self.place( + id, + request.key, + request.mesh.expect("satisfied"), + request.child_existence.expect("satisfied"), + ); + self.entries.insert(request.key, Entry::Leaf(id)); + self.top_index.insert(id, self.top_ids.len()); + self.top_ids.push(id); + } + + /// A node's children came back: a leaf parent gets a block of them and + /// becomes inner; an inner parent's block is remade with the old and new + /// children interleaved in child order. + fn finish_child(&mut self, request_id: usize) { + let request = self.children.release(request_id); + let parent_key = request.key; + let parent = self.entries[&parent_key]; + let (Entry::Leaf(parent_id) | Entry::Inner(parent_id)) = parent else { + unreachable!("a child request's parent is a node"); + }; + if request.mask == 0 { + // Cancelled: every child it waited on left. + self.node_mut(parent_id).request = None; + self.invalidate(parent_id); + return; + } + let fill = |graph: &mut Self, id: u32, child: usize| { + let key = child_key(parent_key, child); + graph.place(id, key, request.meshes[child], request.existence[child]); + let previous = graph.entries.insert(key, Entry::Leaf(id)); + debug_assert!(matches!(previous, Some(Entry::Child(_)))); + }; + match parent { + Entry::Leaf(_) => { + let count = request.mask.count_ones(); + let base = self.allocate(count); + for (id, child) in (base..).zip(set_bits(request.mask)) { + fill(self, id, child); + } + let node = self.node_mut(parent_id); + node.children = Children::At(base); + node.child_count = count as u8; + node.request = None; + node.all_children_leaf = true; + self.entries.insert(parent_key, Entry::Inner(parent_id)); + self.invalidate(parent_id); + if !self.top_keys.contains(&parent_key) { + let Entry::Inner(grandparent) = + self.entries[&parent_key.ancestor(parent_key.level + 1)] + else { + unreachable!("a non-top node's parent is inner"); + }; + self.node_mut(grandparent).all_children_leaf = false; + } + } + Entry::Inner(_) => { + let (old_ptr, old_count, old_existence) = match self.node(parent_id).children { + Children::At(ptr) => { + let count = u32::from(self.node(parent_id).child_count); + (Some(ptr), count, self.block_existence(ptr, count)) + } + Children::Empty => (None, 0, 0), + Children::None => unreachable!("an inner node has a children block"), + }; + debug_assert_eq!( + old_existence | request.mask, + self.node(parent_id).child_existence + ); + debug_assert_eq!(request.mask & old_existence, 0); + let new_mask = request.mask | old_existence; + let new_count = new_mask.count_ones(); + let new_ptr = self.allocate(new_count); + let mut previous = old_ptr.unwrap_or(0); + let children = set_bits(new_mask); + for (next, child) in (new_ptr..).zip(children) { + if request.mask & 1 << child != 0 { + fill(self, next, child); + } else { + self.move_node(previous, next, parent_id); + previous += 1; + } + } + debug_assert!(old_ptr.is_none_or(|ptr| { + (ptr..ptr + old_count).all(|id| self.nodes[id as usize].is_none()) + })); + let node = self.node_mut(parent_id); + if old_ptr.is_none() { + node.all_children_leaf = true; + } + node.children = Children::At(new_ptr); + node.child_count = new_count as u8; + node.request = None; + self.invalidate(parent_id); + } + Entry::Single(_) | Entry::Child(_) => unreachable!("checked above"), + } + } + + /// The traversal asked for `key`: a leaf's children, or an inner node's + /// own mesh. + pub fn process_request(&mut self, key: SectionKey) { + let Some(&entry) = self.entries.get(&key) else { + return; + }; + match entry { + Entry::Single(_) | Entry::Child(_) => {} + Entry::Leaf(id) => { + if key.level == 0 + || self.node(id).mesh == Mesh::Null + || self.node(id).request.is_some() + { + return; + } + self.make_leaf_child_request(id); + } + Entry::Inner(id) => { + if !self.node(id).geometry_in_flight { + if self.node(id).mesh == Mesh::Null { + self.watch.push(key); + self.node_mut(id).geometry_in_flight = true; + } else { + // Already meshed: rewritten so the GPU's request + // flag clears. + self.invalidate(id); + } + } + } + } + } + + /// The cleaner wants `key`'s mesh gone: an inner node loses it, to be + /// asked for again when wanted; a leaf's parent takes the leaf's place, + /// its children removed, unless the parent has no mesh yet, which is + /// asked for instead. A top-level leaf keeps its mesh, nothing standing + /// in for it. + pub fn remove_node_geometry(&mut self, key: SectionKey) { + let Some(&entry) = self.entries.get(&key) else { + return; + }; + match entry { + Entry::Single(_) | Entry::Child(_) => {} + Entry::Inner(id) => self.clear_geometry(key, id), + Entry::Leaf(_) => { + if self.top_keys.contains(&key) { + return; + } + let parent_key = key.ancestor(key.level + 1); + let Entry::Inner(parent) = self.entries[&parent_key] else { + unreachable!("a leaf's parent is inner"); + }; + if self.node(parent).mesh == Mesh::Null { + self.process_request(parent_key); + } else { + self.remove_recursive(parent_key, None, true); + self.entries.insert(parent_key, Entry::Leaf(parent)); + } + } + } + } + + /// An inner node's mesh goes, and the worker stops meshing it. Child + /// changes arrive with meshes, so the node's existence stays as it was + /// until its mesh is asked for again. + fn clear_geometry(&mut self, key: SectionKey, id: u32) { + let mesh = self.node(id).mesh; + if let Mesh::Id(_) = mesh { + self.unwatch.push(key); + self.free_mesh(mesh); + let node = self.node_mut(id); + node.mesh = Mesh::Null; + node.geometry_in_flight = false; + self.invalidate(id); + } + } + + /// A request for each child the existence has, each watched from here + /// on. + fn make_leaf_child_request(&mut self, id: u32) { + let (key, child_existence) = { + let node = self.node(id); + (node.key, node.child_existence) + }; + if child_existence == 0 && !self.top_keys.contains(&key) { + // Nothing under it; the record is rewritten so the GPU's + // request flag clears. A top-level node keeps an open request, + // which a later existence change fills. + self.invalidate(id); + return; + } + let request_id = self.children.put(ChildRequest::new(key)); + self.node_mut(id).request = Some(request_id); + self.invalidate(id); + for child in set_bits(child_existence) { + self.add_requested_child(request_id, key, child); + } + } + + #[cfg(feature = "test-harness")] + pub fn has_column(&self, x: i32, z: i32) -> bool { + let side = super::section::SIDE as i32; + self.nodes.iter().flatten().any(|node| { + node.key.level == 0 + && node.key.x == x.div_euclid(side) + && node.key.z == z.div_euclid(side) + && matches!(node.mesh, Mesh::Id(_)) + }) + } +} + +/// `key`'s index among its parent's children: x in bit 0, z in bit 1, y in +/// bit 2. +pub(crate) fn child_index(key: SectionKey) -> usize { + (key.x & 1 | (key.z & 1) << 1 | (key.y & 1) << 2) as usize +} + +/// The child indices `mask` has set. +fn set_bits(mask: u8) -> impl Iterator { + (0..8).filter(move |i| mask & 1 << i != 0) +} + +pub(crate) fn child_key(key: SectionKey, child: usize) -> SectionKey { + debug_assert!(key.level > 0); + let child = child as i32; + SectionKey { + level: key.level - 1, + x: key.x * 2 + (child & 1), + z: key.z * 2 + (child >> 1 & 1), + y: key.y * 2 + (child >> 2), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::renderer::distant::section::LEVELS; + + fn built(key: SectionKey, quads: usize, child_existence: u8) -> BuiltSection { + let mut section = BuiltSection::empty(key, child_existence); + section.quads = vec![0; quads]; + section + } + + impl NodeGraph { + /// Every record waiting, none held for a pending mesh. + fn take_all(&mut self) -> Vec<(u32, [u32; 4])> { + self.take_dirty(usize::MAX, |_| false) + } + } + + fn top() -> SectionKey { + SectionKey { + level: LEVELS - 1, + x: 0, + y: 0, + z: 0, + } + } + + /// Uploads hand out ids in turn. + fn uploader() -> impl FnMut(BuiltSection) -> Option { + let mut next = 100; + move |_| { + next += 1; + Some(next) + } + } + + #[test] + fn dirty_records_past_the_limit_wait_for_the_next_take() { + let mut graph = NodeGraph::new(); + let other = SectionKey { x: 1, ..top() }; + graph.set_roots(&HashSet::from([top(), other])); + let mut uploader = uploader(); + graph.process_geometry_result(built(top(), 4, 0b101), &mut uploader); + graph.process_geometry_result(built(other, 4, 0b101), &mut uploader); + assert_eq!(graph.take_dirty(1, |_| false).len(), 1); + assert_eq!(graph.take_all().len(), 1); + assert!(graph.take_all().is_empty()); + } + + #[test] + fn a_parent_waits_for_its_childrens_records() { + let mut graph = NodeGraph::new(); + let mut upload = uploader(); + graph.set_roots(&HashSet::from([top()])); + graph.process_geometry_result(built(top(), 4, 0b11), &mut upload); + graph.take_all(); + graph.process_request(top()); + graph.take_all(); + graph.process_geometry_result(built(child_key(top(), 0), 2, 0), &mut upload); + graph.process_geometry_result(built(child_key(top(), 1), 2, 0), &mut upload); + assert_eq!(graph.entries[&top()], Entry::Inner(0)); + + // One record a take: the children first, the parent only after both. + let ids: Vec = (0..3) + .map(|_| { + let taken = graph.take_dirty(1, |_| false); + assert_eq!(taken.len(), 1); + taken[0].0 + }) + .collect(); + assert_eq!(ids[2], 0, "{ids:?}"); + assert_eq!(graph.written_through(), graph.dirty_mark()); + } + + #[test] + fn a_node_naming_a_pending_mesh_waits_with_it() { + let mut graph = NodeGraph::new(); + graph.set_roots(&HashSet::from([top()])); + graph.process_geometry_result(built(top(), 4, 0), &mut uploader()); + assert!(graph.take_dirty(usize::MAX, |mesh| mesh == 101).is_empty()); + // Its id stays out of the traversal until the GPU has its record. + assert!(graph.written_top_ids().is_empty()); + assert!(graph.written_through() < graph.dirty_mark()); + assert_eq!(graph.take_all().len(), 1); + assert_eq!(graph.written_top_ids(), [0]); + assert_eq!(graph.written_through(), graph.dirty_mark()); + } + + #[test] + fn a_freed_root_goes_while_another_record_waits() { + let mut graph = NodeGraph::new(); + let mut upload = uploader(); + let other = SectionKey { x: 1, ..top() }; + graph.set_roots(&HashSet::from([top(), other])); + graph.process_geometry_result(built(top(), 4, 0), &mut upload); + graph.process_geometry_result(built(other, 4, 0), &mut upload); + graph.take_all(); + + // One root goes while the other's new mesh is pending: nothing + // points at a root's record, so it goes at once. + graph.set_roots(&HashSet::from([top()])); + graph.process_geometry_result(built(top(), 4, 0), &mut upload); + let taken = graph.take_dirty(usize::MAX, |mesh| mesh == 103); + assert_eq!(taken, [(1, [u32::MAX; 4])]); + assert_eq!(graph.take_all()[0].1[2] & 0xFF_FFFF, 103); + } + + #[test] + fn freed_ids_wait_only_on_their_old_parent_and_the_limit() { + let mut graph = NodeGraph::new(); + let mut upload = uploader(); + let other = SectionKey { x: 1, ..top() }; + graph.set_roots(&HashSet::from([top(), other])); + graph.process_geometry_result(built(top(), 4, 0b11), &mut upload); + graph.process_geometry_result(built(other, 4, 0), &mut upload); + graph.process_request(top()); + graph.process_geometry_result(built(child_key(top(), 0), 2, 0), &mut upload); + graph.process_geometry_result(built(child_key(top(), 1), 2, 0), &mut upload); + graph.take_all(); + assert_eq!(graph.entries[&top()], Entry::Inner(0)); + + // The other root waits on its new mesh (105); the first drops child + // 1, so its block (2, 3) is remade at 4. + graph.process_geometry_result(built(other, 4, 0), &mut upload); + graph.process_geometry_result(built(top(), 4, 0b01), &mut upload); + let ids = |records: Vec<(u32, [u32; 4])>| { + let mut ids: Vec = records.into_iter().map(|(id, _)| id).collect(); + ids.sort_unstable(); + ids + }; + // The limit leaves no room for the freed ids. + assert_eq!(ids(graph.take_dirty(2, |mesh| mesh == 105)), [0, 4]); + // Their parent written, they go though the other root still waits. + assert_eq!( + ids(graph.take_dirty(usize::MAX, |mesh| mesh == 105)), + [2, 3] + ); + assert_eq!(ids(graph.take_all()), [1]); + } + + #[test] + fn a_pick_whose_id_moved_or_was_freed_is_stale() { + let mut graph = NodeGraph::new(); + let mut upload = uploader(); + graph.set_roots(&HashSet::from([top()])); + graph.process_geometry_result(built(top(), 4, 0b11), &mut upload); + graph.process_request(top()); + let [first, second] = [0, 1].map(|i| child_key(top(), i)); + graph.process_geometry_result(built(first, 2, 0), &mut upload); + graph.process_geometry_result(built(second, 2, 0), &mut upload); + assert!(graph.holds(1, first) && graph.holds(2, second)); + assert!(!graph.holds(1, second)); + + // The second child goes and the first moves to 3. + graph.process_geometry_result(built(top(), 4, 0b01), &mut upload); + assert!(!graph.holds(1, first)); + assert!(!graph.holds(2, second)); + assert!(graph.holds(3, first)); + assert!(!graph.holds(CLEANER_SENTINEL, top())); + } + + #[test] + fn a_freed_id_is_not_reused_before_its_old_parent_is_written() { + let mut graph = NodeGraph::new(); + let mut upload = uploader(); + graph.set_roots(&HashSet::from([top()])); + graph.process_geometry_result(built(top(), 4, 0b11), &mut upload); + graph.process_request(top()); + graph.process_geometry_result(built(child_key(top(), 0), 2, 0), &mut upload); + graph.process_geometry_result(built(child_key(top(), 1), 2, 0), &mut upload); + graph.take_all(); + + // Re-meshed without child 1, its new mesh (104) pending: the block is + // remade at 3, and 1 and 2 freed while the parent naming them waits. + graph.process_geometry_result(built(top(), 4, 0b01), &mut upload); + let taken: Vec = graph + .take_dirty(usize::MAX, |mesh| mesh == 104) + .into_iter() + .map(|(id, _)| id) + .collect(); + assert_eq!(taken, [3]); + let other = SectionKey { x: 1, ..top() }; + graph.set_roots(&HashSet::from([top(), other])); + graph.process_geometry_result(built(other, 4, 0), &mut upload); + assert_eq!(graph.entries[&other], Entry::Leaf(4)); + + let taken: HashMap = graph.take_all().into_iter().collect(); + assert_eq!(taken[&1], [u32::MAX; 4]); + assert_eq!(taken[&2], [u32::MAX; 4]); + assert_eq!(graph.allocate(1), 1); + } + + #[test] + fn written_through_passes_a_clear_while_later_records_wait() { + let mut graph = NodeGraph::new(); + let mut upload = uploader(); + graph.set_roots(&HashSet::from([top()])); + graph.process_geometry_result(built(top(), 4, 0), &mut upload); + graph.take_all(); + graph.clear(); + let cleared = graph.dirty_mark(); + assert!(graph.take_dirty(0, |_| false).is_empty()); + + // A new root waits on its mesh (102); the cleared record goes anyway. + let other = SectionKey { x: 1, ..top() }; + graph.set_roots(&HashSet::from([other])); + graph.process_geometry_result(built(other, 4, 0), &mut upload); + let taken = graph.take_dirty(usize::MAX, |mesh| mesh == 102); + assert_eq!(taken, [(0, [u32::MAX; 4])]); + assert!(graph.written_through() >= cleared); + assert!(graph.written_through() < graph.dirty_mark()); + } + + #[test] + fn no_node_is_given_the_cleaner_sentinel() { + let mut ids = IdSet::new(NODE_IDS); + assert_eq!(ids.alloc(NODE_IDS), Some(0)); + assert_eq!(ids.alloc(1), None); + const { assert!(CLEANER_SENTINEL >= NODE_IDS && CLEANER_SENTINEL < MAX_NODES) }; + } + + #[test] + fn a_root_is_requested_then_becomes_a_top_level_leaf() { + let mut graph = NodeGraph::new(); + graph.set_roots(&HashSet::from([top()])); + assert_eq!(graph.watch, vec![top()]); + assert_eq!(graph.in_flight(), 1); + graph.process_geometry_result(built(top(), 4, 0b101), &mut uploader()); + assert_eq!(graph.in_flight(), 0); + assert_eq!(graph.top_ids(), &[0]); + let dirty = graph.take_all(); + let (id, record) = dirty[0]; + assert_eq!(id, 0); + let key = top().pack(); + assert_eq!(record, [(key >> 32) as u32, key as u32, 101, NULL]); + } + + #[test] + fn a_leaf_request_makes_its_children_and_an_inner_node() { + let mut graph = NodeGraph::new(); + let mut upload = uploader(); + graph.set_roots(&HashSet::from([top()])); + graph.process_geometry_result(built(top(), 4, 0b0000_0101), &mut upload); + graph.watch.clear(); + graph.take_all(); + + graph.process_request(top()); + let children: Vec = [0, 2].map(|i| child_key(top(), i)).to_vec(); + assert_eq!(graph.watch, children); + // The request shows on the GPU as the flag, so it isn't asked again. + let (_, record) = graph.take_all()[0]; + assert_eq!(record[2] >> 24, 1); + graph.process_request(top()); + assert!(graph.take_all().is_empty()); + + graph.process_geometry_result(built(children[0], 0, 0), &mut upload); + assert_eq!(graph.in_flight(), 1); + graph.process_geometry_result(built(children[1], 3, 0b1), &mut upload); + assert_eq!(graph.in_flight(), 0); + let records: HashMap = graph.take_all().into_iter().collect(); + let parent = records[&0]; + assert_eq!(parent[2] & 0xFF_FFFF, 101); + // Two children from id 1, all leaves, no request. + assert_eq!(parent[2] >> 24, 1 << 2 | 1 << 5); + assert_eq!(parent[3], 1); + assert_eq!(records[&1][2] & 0xFF_FFFF, EMPTY); + assert_eq!(records[&2][2] & 0xFF_FFFF, 102); + assert_eq!(records[&2][3] & 0xFF_FFFF, NULL); + assert_eq!(graph.entries[&top()], Entry::Inner(0)); + } + + #[test] + fn removing_a_root_frees_its_tree_and_meshes() { + let mut graph = NodeGraph::new(); + graph.set_roots(&HashSet::from([top()])); + let mut upload = uploader(); + graph.process_geometry_result(built(top(), 4, 0b1), &mut upload); + graph.process_request(top()); + graph.process_geometry_result(built(child_key(top(), 0), 2, 0), &mut upload); + graph.watch.clear(); + graph.take_all(); + + graph.set_roots(&HashSet::new()); + assert!(graph.entries.is_empty()); + assert!(graph.top_ids().is_empty()); + assert_eq!(graph.unwatch, vec![child_key(top(), 0), top()]); + let mut freed = graph.freed.clone(); + freed.sort_unstable(); + assert_eq!(freed, vec![101, 102]); + // The ids come free as their records go out. + assert_eq!(graph.ids.used, 2); + let records = graph.take_all(); + assert!(records.iter().all(|&(_, record)| record == [u32::MAX; 4])); + assert_eq!(graph.ids.used, 0); + } + + #[test] + fn the_cleaner_strips_an_inner_node_and_folds_a_leaf_into_its_parent() { + let mut graph = NodeGraph::new(); + graph.set_roots(&HashSet::from([top()])); + let mut upload = uploader(); + graph.process_geometry_result(built(top(), 4, 0b1), &mut upload); + graph.process_request(top()); + let child = child_key(top(), 0); + graph.process_geometry_result(built(child, 2, 0), &mut upload); + let reset = |id| id | VISIBILITY_RESET_BIT; + assert_eq!(graph.visibility_sets, HashSet::from([reset(0), reset(1)])); + graph.watch.clear(); + graph.take_all(); + + // An inner node: its mesh goes, its children stay. + graph.remove_node_geometry(top()); + assert_eq!(graph.freed, vec![101]); + assert_eq!(graph.unwatch, vec![top()]); + let (_, record) = graph.take_all()[0]; + assert_eq!(record[2] & 0xFF_FFFF, NULL); + assert_eq!(graph.entries[&top()], Entry::Inner(0)); + + // A leaf whose parent has no mesh: the parent is asked for instead. + graph.remove_node_geometry(child); + assert_eq!(graph.watch, vec![top()]); + assert_eq!(graph.entries[&child], Entry::Leaf(1)); + + // With the parent meshed, the leaf goes and the parent is a leaf. + graph.process_geometry_result(built(top(), 4, 0b1), &mut upload); + graph.freed.clear(); + graph.unwatch.clear(); + graph.remove_node_geometry(child); + assert_eq!(graph.entries[&top()], Entry::Leaf(0)); + assert!(!graph.entries.contains_key(&child)); + assert_eq!(graph.freed, vec![102]); + assert_eq!(graph.unwatch, vec![child]); + graph.take_all(); + assert_eq!(graph.ids.used, 1); + assert_eq!(graph.visibility_sets, HashSet::from([reset(0), 1])); + + // A top-level leaf keeps its mesh. + graph.remove_node_geometry(top()); + assert_eq!(graph.freed, vec![102]); + assert_eq!(graph.entries[&top()], Entry::Leaf(0)); + } + + #[test] + fn a_grown_existence_adds_children_to_an_inner_node_in_order() { + let mut graph = NodeGraph::new(); + graph.set_roots(&HashSet::from([top()])); + let mut upload = uploader(); + graph.process_geometry_result(built(top(), 4, 0b100), &mut upload); + graph.process_request(top()); + graph.process_geometry_result(built(child_key(top(), 2), 2, 0), &mut upload); + graph.take_all(); + + // The root re-meshed with a new child before the old one. + graph.process_geometry_result(built(top(), 4, 0b101), &mut upload); + assert_eq!(graph.in_flight(), 1); + graph.process_geometry_result(built(child_key(top(), 0), 2, 0), &mut upload); + let records: HashMap = graph.take_all().into_iter().collect(); + let Entry::Inner(root) = graph.entries[&top()] else { + panic!("root is inner"); + }; + let ptr = records[&root][3] & 0xFF_FFFF; + assert_eq!(records[&root][2] >> 24 & 0b11100, 1 << 2); + assert_eq!(graph.entries[&child_key(top(), 0)], Entry::Leaf(ptr)); + assert_eq!(graph.entries[&child_key(top(), 2)], Entry::Leaf(ptr + 1)); + } +} diff --git a/pomme-client/src/renderer/distant/scan.rs b/pomme-client/src/renderer/distant/scan.rs new file mode 100644 index 000000000..3fe9cb73e --- /dev/null +++ b/pomme-client/src/renderer/distant/scan.rs @@ -0,0 +1,346 @@ +//! The plane mesher: a 32-wide plane of voxel faces fed one at a time, row +//! by row, merged greedily into quads of up to 16x16 and written out as +//! packed quads. + +/// A quad's bits: face in 0-2 (`axis << 1 | side`, axis 0 for y, 1 for z, +/// 2 for x), width-1 in 3-6, height-1 in 7-10, z in 11-15, y in 16-20, x in +/// 21-25, model id in 26-45 (ids are u16, so 42-45 stay clear), biome in +/// 46-54, light in 55-62. +pub(crate) const QUAD_MODEL_SHIFT: u32 = 26; +pub(crate) const QUAD_BIOME_SHIFT: u32 = 46; +pub(crate) const QUAD_LIGHT_SHIFT: u32 = 55; +/// The quad's light byte: sky in the low nibble, block in the high. +pub(crate) const QUAD_LIGHT: u64 = 0xFF << QUAD_LIGHT_SHIFT; +/// The bits below the model id, which the mesher uses for the face's side +/// (bit 0) and its group (bits 1-2) until the quad is placed. +const AUX: u64 = (1 << QUAD_MODEL_SHIFT) - 1; + +const MAX_SIZE: u8 = 16; + +/// The groups a section's quads come in: translucent, double-sided, then +/// one per face. +pub(crate) const GROUPS: usize = 8; + +/// Where a section's quads land, and the bounds they span. +pub(crate) struct Quads { + pub groups: [Vec; GROUPS], + pub min: [i32; 3], + pub max: [i32; 3], +} + +impl Quads { + pub fn new() -> Self { + Self { + groups: Default::default(), + min: [i32::MAX; 3], + max: [i32::MIN; 3], + } + } + + pub fn clear(&mut self) { + for group in &mut self.groups { + group.clear(); + } + self.min = [i32::MAX; 3]; + self.max = [i32::MIN; 3]; + } + + pub fn count(&self) -> usize { + self.groups.iter().map(Vec::len).sum() + } +} + +/// One plane's mesher. `axis` is the plane's normal; `position` which layer +/// along it, and `face_offset` whether a face on the negative side belongs +/// to the next layer up (the inner passes) or to this one (the outer). +pub(crate) struct Mesher { + row_data: [u64; 32], + row_length: [u8; 32], + row_depth: [u8; 32], + row_bitset: u32, + index: u32, + sum: u8, + data: u64, + pub axis: u8, + pub position: u8, + pub face_offset: bool, +} + +impl Mesher { + pub fn new(axis: u8, position: u8, face_offset: bool) -> Self { + Self { + row_data: [0; 32], + row_length: [0; 32], + row_depth: [0; 32], + row_bitset: 0, + index: 0, + sum: 0, + data: 0, + axis, + position, + face_offset, + } + } + + /// The next face along the row; 0 for none. + pub fn put(&mut self, data: u64, out: &mut Quads) { + let idx = (self.index & 31) as usize; + self.index += 1; + if idx == 0 { + // A row starts: what the last row ended on couldn't merge + // sideways, so it goes in slot 31. + if self.data != 0 { + if self.row_bitset & (1 << 31) != 0 { + self.emit(31, ((self.index - 1) >> 5) - 1, out); + } + self.row_bitset |= 1 << 31; + self.row_length[31] = self.sum; + self.row_depth[31] = 1; + self.row_data[31] = self.data; + } + self.data = data; + self.sum = 0; + } + if data != self.data || self.sum == MAX_SIZE { + if self.data != 0 { + let prev = idx - 1; + self.row_depth[prev] = 1; + self.row_length[prev] = self.sum; + self.row_data[prev] = self.data; + self.row_bitset |= 1 << prev; + } + self.data = data; + self.sum = 0; + } + self.sum += 1; + + let is_set = self.row_bitset & (1 << idx) != 0; + let mut by_depth_max = false; + // The run above, ending here with the same data and length, takes + // this one. + if self.data != 0 + && is_set + && self.row_length[idx] == self.sum + && self.row_data[idx] == self.data + { + self.row_depth[idx] += 1; + self.sum = 0; + self.data = 0; + if self.row_depth[idx] != MAX_SIZE { + return; + } + by_depth_max = true; + } + if is_set { + let row = ((self.index - 1) >> 5) - u32::from(!by_depth_max); + self.emit(idx, row, out); + self.row_bitset &= !(1 << idx); + } + } + + /// Writes out every pending run in `mask`'s slots. + fn emit_ranged(&mut self, mask: u32, out: &mut Quads) { + let mut set = self.row_bitset & mask; + self.row_bitset &= !mask; + while set != 0 { + let idx = set.trailing_zeros() as usize; + set &= set - 1; + self.emit(idx, (self.index >> 5) - 1, out); + } + } + + /// `count` faces of nothing. + pub fn skip(&mut self, mut count: u32, out: &mut Quads) { + if count == 0 { + return; + } + if self.data != 0 { + self.put(0, out); + count -= 1; + } + if count > 0 { + let mask = (((1u64 << count.min(32)) - 1) << (self.index & 31)) as u32; + self.emit_ranged(mask, out); + self.index += count; + } + } + + pub fn end_row(&mut self, out: &mut Quads) { + if self.index & 31 != 0 { + self.skip(32 - (self.index & 31), out); + } + } + + /// Writes out whatever is pending and starts over. + pub fn finish(&mut self, out: &mut Quads) { + if self.index != 0 { + self.skip(32 - (self.index & 31), out); + self.emit_ranged(u32::MAX, out); + } + self.row_bitset = 0; + self.sum = 0; + self.data = 0; + self.index = 0; + } + + /// The run ending at column `x` of row `z`, `length` along the row and + /// `width` rows deep, placed as a quad. + fn emit(&self, x: usize, z: u32, out: &mut Quads) { + let (mut length, mut width) = (u32::from(self.row_length[x]), u32::from(self.row_depth[x])); + let data = self.row_data[x]; + let mut x = x as u32 + 1 - length; + let mut z = z + 1 - width; + if self.axis == 2 { + // Along x, rows run up y and columns along z. + std::mem::swap(&mut x, &mut z); + std::mem::swap(&mut length, &mut width); + } + let aux = (data & AUX) as u32; + let data = data & !AUX; + let side = aux & 1; + let group = aux >> 1 & 3; + let position = u32::from(self.position) + 1 - if self.face_offset { side } else { 1 }; + let axis = u32::from(self.axis); + let face = axis << 1 | side; + let mut encoded = face | (width - 1) << 7 | (length - 1) << 3; + encoded |= x << if axis == 2 { 16 } else { 21 }; + encoded |= z << if axis == 1 { 16 } else { 11 }; + encoded |= position + << match axis { + 0 => 16, + 1 => 11, + _ => 21, + }; + let group = group as usize + if group == 2 { face as usize } else { 0 }; + out.groups[group].push(data | u64::from(encoded)); + + let (position, x, z, length, width) = ( + position as i32, + x as i32, + z as i32, + length as i32, + width as i32, + ); + // Along the normal the bounds sit flat on the face's layer, with no + // depth of their own. + let (lo, hi) = match axis { + 0 => ([x, position, z], [x + length, position, z + width]), + 1 => ([x, z, position], [x + length, z + width, position]), + _ => ([position, x, z], [position, x + length, z + width]), + }; + for i in 0..3 { + out.min[i] = out.min[i].min(lo[i]); + out.max[i] = out.max[i].max(hi[i]); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Feeds a 32x32 plane of face data, row by row, and returns each + /// quad's `(x, z, length, width, data)`, the data's aux bits cleared. + fn mesh(plane: &[u64; 1024]) -> Vec<(u32, u32, u32, u32, u64)> { + let mut mesher = Mesher::new(0, 0, true); + let mut out = Quads::new(); + for &data in plane { + if data == 0 { + mesher.skip(1, &mut out); + } else { + mesher.put(data, &mut out); + } + } + mesher.finish(&mut out); + out.groups + .iter() + .flatten() + .map(|&quad| { + let field = |shift: u32, bits: u32| (quad >> shift) as u32 & ((1 << bits) - 1); + ( + field(21, 5), + field(11, 5), + field(3, 4) + 1, + field(7, 4) + 1, + quad & !((1 << 26) - 1), + ) + }) + .collect() + } + + const A: u64 = 1 << 26; + const B: u64 = 2 << 26; + + #[test] + fn runs_merge_across_rows_up_to_sixteen() { + let mut plane = [0u64; 1024]; + for z in 0..20 { + for x in 0..20 { + plane[z * 32 + x] = A; + } + } + let mut quads = mesh(&plane); + quads.sort_unstable(); + assert_eq!( + quads, + [ + (0, 0, 16, 16, A), + (0, 16, 16, 4, A), + (16, 0, 4, 16, A), + (16, 16, 4, 4, A) + ] + ); + } + + #[test] + fn every_face_is_covered_exactly_once() { + // A fixed pseudo-random plane of two kinds of face and gaps. + let mut seed = 12345u32; + let plane: [u64; 1024] = std::array::from_fn(|_| { + seed = seed.wrapping_mul(1_103_515_245).wrapping_add(12345); + match seed >> 28 { + 0..=5 => A, + 6..=12 => B, + _ => 0, + } + }); + let mut covered = [0u64; 1024]; + for (x, z, length, width, data) in mesh(&plane) { + assert!((1..=16).contains(&length) && (1..=16).contains(&width)); + for dz in 0..width { + for dx in 0..length { + let at = ((z + dz) * 32 + x + dx) as usize; + assert_eq!(covered[at], 0, "face {at} covered twice"); + covered[at] = data; + } + } + } + assert_eq!(covered, plane); + } + + #[test] + fn quads_land_on_their_face_plane() { + // The negative face of an inner layer belongs to the layer above. + let mut out = Quads::new(); + let mut mesher = Mesher::new(0, 5, true); + mesher.put(A | 1, &mut out); + mesher.finish(&mut out); + mesher.put(A, &mut out); + mesher.finish(&mut out); + let positions: Vec = out.groups.iter().flatten().map(|q| q >> 16 & 31).collect(); + assert_eq!(positions, [5, 6]); + assert_eq!(out.min, [0, 5, 0]); + assert_eq!(out.max, [1, 6, 1]); + // Along x the row runs up y and the column along z. + let mut out = Quads::new(); + let mut mesher = Mesher::new(2, 7, false); + mesher.skip(3, &mut out); + mesher.put(B | 0b101, &mut out); + mesher.finish(&mut out); + let quad = out.groups[7][0]; + assert_eq!( + (quad >> 21 & 31, quad >> 16 & 31, quad >> 11 & 31), + (7, 0, 3) + ); + } +} diff --git a/pomme-client/src/renderer/distant/section.rs b/pomme-client/src/renderer/distant/section.rs new file mode 100644 index 000000000..224438bbe --- /dev/null +++ b/pomme-client/src/renderer/distant/section.rs @@ -0,0 +1,617 @@ +//! Distant terrain's world sections: 32x32x32 voxels at each of five levels +//! of detail, what a vanilla section becomes at every level before it's +//! written in, and a section's file. + +use std::io::{Read, Write}; + +use super::voxel::{self, States}; +use crate::world::chunk::ChunkLightData; + +/// Levels of detail: at level `l` a voxel is `1 << l` blocks wide. +pub(crate) const LEVELS: u8 = 5; +pub(crate) const SIDE: usize = 32; +pub(crate) const VOLUME: usize = SIDE * SIDE * SIDE; + +/// A section at one level, in section coordinates of that level. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub(crate) struct SectionKey { + pub level: u8, + pub x: i32, + pub y: i32, + pub z: i32, +} + +impl SectionKey { + /// The level-`level` section holding vanilla section `x`/`y`/`z`. + pub fn of_vanilla(level: u8, x: i32, y: i32, z: i32) -> Self { + let shift = level + 1; + Self { + level, + x: x >> shift, + y: y >> shift, + z: z >> shift, + } + } + + /// The key as one word: level in the top nibble, then y (8 bits), z and + /// x (24 each), 4 bits spare. + pub fn pack(self) -> u64 { + u64::from(self.level) << 60 + | (self.y as u64 & 0xFF) << 52 + | (self.z as u64 & 0xFF_FFFF) << 28 + | (self.x as u64 & 0xFF_FFFF) << 4 + } + + pub fn unpack(id: u64) -> Self { + Self { + level: (id >> 60) as u8, + x: ((id << 36) as i64 >> 40) as i32, + y: ((id << 4) as i64 >> 56) as i32, + z: ((id << 12) as i64 >> 40) as i32, + } + } + + /// The level-`level` section this one lies in. + pub fn ancestor(self, level: u8) -> Self { + let shift = level - self.level; + Self { + level, + x: self.x >> shift, + y: self.y >> shift, + z: self.z >> shift, + } + } + + /// The section of the same level `dx`/`dy`/`dz` sections away. + pub fn offset(self, [dx, dy, dz]: [i32; 3]) -> Self { + Self { + x: self.x + dx, + y: self.y + dy, + z: self.z + dz, + ..self + } + } + + /// Blocks along a side. + pub fn width(self) -> i32 { + (SIDE as i32) << self.level + } + + /// The block position of the section's corner. + pub fn origin(self) -> [i32; 3] { + [self.x, self.y, self.z].map(|c| c * self.width()) + } + + /// Which of its parent's eight children this is. + pub fn child_index(self) -> u8 { + (self.x & 1 | (self.z & 1) << 1 | (self.y & 1) << 2) as u8 + } + + /// The horizontal distance from `x`/`z` to the section's nearest edge. + pub fn distance(self, x: f64, z: f64) -> f64 { + let [ox, _, oz] = self.origin(); + let width = f64::from(self.width()); + let dx = (f64::from(ox) - x).max(x - f64::from(ox) - width).max(0.0); + let dz = (f64::from(oz) - z).max(z - f64::from(oz) - width).max(0.0); + dx.hypot(dz) + } +} + +/// Air under full sky, which an unseen section is made of. +pub(crate) const UNSEEN: u64 = voxel::compose(0x0F, voxel::AIR, 0); + +/// A voxel's index: `y`, then `z`, then `x`. +pub(crate) fn index(x: usize, y: usize, z: usize) -> usize { + y << 10 | z << 5 | x +} + +/// 32³ voxels at one level. +pub(crate) struct WorldSection { + pub data: Box<[u64]>, + /// A bit per child octant with anything in it; at level 0, 0xFF if any + /// voxel is a block. + pub non_empty_children: u8, + /// Level 0 only: how many voxels are blocks. + pub non_empty_count: u32, +} + +impl WorldSection { + /// What a section nothing was seen in holds: air under full sky. + pub fn unseen() -> Self { + Self { + data: vec![UNSEEN; VOLUME].into(), + non_empty_children: 0, + non_empty_count: 0, + } + } + + /// Whether it still holds only that, so a file for it says nothing. + pub fn is_unseen(&self) -> bool { + self.non_empty_children == 0 && self.data.iter().all(|&v| v == UNSEEN) + } + + /// Level 0's `non_empty_children` from its count; whether it changed. + fn update_lvl0_state(&mut self) -> bool { + let next = if self.non_empty_count == 0 { 0 } else { 0xFF }; + std::mem::replace(&mut self.non_empty_children, next) != next + } + + /// Records whether child `index` has anything in it: 0 for no change, + /// 1 for a change, 2 for this section going empty or non-empty. + fn update_empty_child_state(&mut self, index: u8, child_non_empty: bool) -> u8 { + let mask = 1 << index; + let prev = self.non_empty_children; + let next = prev & !mask | if child_non_empty { mask } else { 0 }; + self.non_empty_children = next; + if (prev != 0) != (next != 0) { + 2 + } else { + u8::from(prev != next) + } + } +} + +/// Sides of the five levels of a vanilla section: 16, 8, 4, 2, 1. +const fn level_side(level: u8) -> usize { + 1 << level_shift(level) +} + +/// `log2` of [`level_side`]. +const fn level_shift(level: u8) -> u32 { + 4 - level as u32 +} + +/// Voxel `i` of a cube `1 << shift` wide laid out `y`-major, then `z`, then +/// `x`. +fn position(i: usize, shift: u32) -> (usize, usize, usize) { + let mask = (1 << shift) - 1; + (i & mask, i >> (2 * shift), i >> shift & mask) +} + +const fn level_base(level: u8) -> usize { + let mut base = 0; + let mut l = 0; + while l < level { + base += level_side(l).pow(3); + l += 1; + } + base +} + +const WORK_VOLUME: usize = level_base(LEVELS); + +/// A vanilla 16³ section as voxels at every level, mipped down to one. +pub(crate) struct VoxelizedSection { + /// Vanilla section coordinates. + pub x: i32, + pub y: i32, + pub z: i32, + pub non_air_count: u32, + data: Box<[u64; WORK_VOLUME]>, +} + +impl VoxelizedSection { + pub fn new(x: i32, y: i32, z: i32) -> Self { + Self { + x, + y, + z, + non_air_count: 0, + data: Box::new([0; WORK_VOLUME]), + } + } + + /// The level's voxels, `side³` of them, `y`-major then `z` then `x`. + pub fn level(&self, level: u8) -> &[u64] { + let base = level_base(level); + &self.data[base..base + level_side(level).pow(3)] + } + + /// `section`'s blocks, biomes and light as level 0; `light` gives a + /// block's `block << 4 | sky`. + pub fn convert( + &mut self, + states: &States, + section: &pomme_protocol::section::Section, + light: impl Fn(usize, usize, usize) -> u8, + ) { + let biomes: [u32; 64] = std::array::from_fn(|i| { + let (x, y, z) = position(i, 2); + voxel::biome(section.biome(x as u32, y as u32, z as u32)) + }); + self.non_air_count = 0; + for i in 0..level_side(0).pow(3) { + let (x, y, z) = position(i, level_shift(0)); + let block = states.block(section.states.get(x as u32, y as u32, z as u32)); + self.non_air_count += u32::from(block != voxel::AIR); + let biome = biomes[(y >> 2) << 4 | (z >> 2) << 2 | x >> 2]; + self.data[i] = voxel::compose(light(x, y, z), block, biome); + } + } + + /// Air, unlit. + pub fn zero(&mut self) { + self.non_air_count = 0; + self.data.fill(0); + } + + /// Level 0 all `voxel`, mipped as if every level were too. + #[cfg(test)] + pub fn fill_for_test(&mut self, voxel: u64, non_air_count: u32) { + self.data.fill(voxel); + self.non_air_count = non_air_count; + } + + /// Each level from the one below, eight voxels to one. + pub fn mip(&mut self, states: &States) { + for level in 1..LEVELS { + let (from, to) = (level_base(level - 1), level_base(level)); + let finer = level_side(level - 1); + for i in 0..level_side(level).pow(3) { + let (x, y, z) = position(i, level_shift(level)); + let child = |dx: usize, dy: usize, dz: usize| { + self.data + [from + (y * 2 + dy) * finer * finer + (z * 2 + dz) * finer + x * 2 + dx] + }; + self.data[to + i] = states.mip( + std::array::from_fn(|c| child(c >> 2, c >> 1 & 1, c & 1)), + level, + ); + } + } + } +} + +/// A column's light at vanilla section `section_y`: `block << 4 | sky`, read +/// as the game reads it. A departure: the reference reads each section's +/// stored layers raw, a missing one as dark, and takes an air section with +/// neither layer as unlit air. Sections above a column's highest sky layer +/// store none, so faces bordering them would draw dark where the game shows +/// open sky; here they read 15, and a missing layer below defers up to the +/// next stored one, as the game's sky light does. +pub(crate) fn light_of( + light: &ChunkLightData, + section_y: i32, +) -> impl Fn(usize, usize, usize) -> u8 + '_ { + move |x, y, z| { + let (x, y, z) = (x as i32, section_y * 16 + y as i32, z as i32); + light.get_block_light(x, y, z) << 4 | light.get_sky_light(x, y, z) + } +} + +/// Writes `section` over `into` where the vanilla section lands at the +/// section's level: whether anything changed, and how many blocks the +/// covered voxels held before. +pub(crate) fn insert_level( + section: &VoxelizedSection, + into: &mut WorldSection, + level: u8, +) -> (bool, u32) { + let mask = (1 << (level + 1)) - 1; + let base = |c: i32| ((c & mask) << level_shift(level)) as usize; + let (bx, by, bz) = (base(section.x), base(section.y), base(section.z)); + let (mut changed, mut non_air_before) = (false, 0); + for (i, &new) in section.level(level).iter().enumerate() { + let (x, y, z) = position(i, level_shift(level)); + let slot = &mut into.data[index(bx + x, by + y, bz + z)]; + non_air_before += u32::from(!voxel::is_air(*slot)); + changed |= *slot != new; + *slot = new; + } + (changed, non_air_before) +} + +/// The per-level bookkeeping after [`insert_level`]: level 0's count and +/// emptiness; whether the section changed and whether its emptiness flipped +/// (bubbled to the parent). +pub(crate) fn settle_level( + into: &mut WorldSection, + level: u8, + changed: bool, + non_air_before: u32, + non_air_now: u32, + child: Option<(u8, bool)>, +) -> (bool, u8) { + let mut emptiness = match child { + Some((index, non_empty)) if level != 0 => into.update_empty_child_state(index, non_empty), + _ => 0, + }; + if level == 0 && non_air_now != non_air_before { + into.non_empty_count = into.non_empty_count + non_air_now - non_air_before; + emptiness = if into.update_lvl0_state() { 2 } else { 0 }; + } + (changed || emptiness != 0, emptiness) +} + +/// A section's file: the key, a palette's size and the non-empty children, +/// a `u16` palette index per voxel, then the palette; little-endian, +/// zlib-compressed. +pub(crate) fn encode(key: SectionKey, section: &WorldSection) -> Vec { + let mut palette: Vec = Vec::new(); + let mut lookup = std::collections::HashMap::new(); + let mut indices = Vec::with_capacity(VOLUME * 2); + for &voxel in §ion.data { + let next = palette.len(); + let index = *lookup.entry(voxel).or_insert_with(|| { + palette.push(voxel); + next + }); + indices.extend((index as u16).to_le_bytes()); + } + let meta = palette.len() as u64 | u64::from(section.non_empty_children) << 16; + let mut out = Vec::with_capacity(16 + indices.len() + palette.len() * 8); + out.extend(key.pack().to_le_bytes()); + out.extend(meta.to_le_bytes()); + out.extend(indices); + for voxel in palette { + out.extend(voxel.to_le_bytes()); + } + let mut zlib = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::fast()); + zlib.write_all(&out) + .and_then(|()| zlib.finish()) + .expect("compressing into memory") +} + +/// The section [`encode`] wrote for `key`; `None` for another's or a +/// damaged file. +pub(crate) fn decode(key: SectionKey, file: &[u8]) -> Option { + let mut bytes = Vec::new(); + flate2::read::ZlibDecoder::new(file) + .read_to_end(&mut bytes) + .ok()?; + let mut rest = bytes.as_slice(); + if u64::from_le_bytes(take(&mut rest)?) != key.pack() { + return None; + } + let meta = u64::from_le_bytes(take(&mut rest)?); + let palette_len = (meta & 0xFFFF) as usize; + let (indices, after) = rest.split_at_checked(VOLUME * 2)?; + let palette: Vec = after + .as_chunks::<8>() + .0 + .iter() + .take(palette_len) + .map(|&bytes| u64::from_le_bytes(bytes)) + .collect(); + if palette.len() != palette_len { + return None; + } + let data: Option> = indices + .as_chunks::<2>() + .0 + .iter() + .map(|&bytes| palette.get(u16::from_le_bytes(bytes) as usize).copied()) + .collect(); + let data = data?; + let non_empty_count = if key.level == 0 { + data.iter().filter(|&&v| !voxel::is_air(v)).count() as u32 + } else { + 0 + }; + Some(WorldSection { + data, + non_empty_children: (meta >> 16) as u8, + non_empty_count, + }) +} + +/// The next `N` bytes of `rest`, taking them off it. +fn take(rest: &mut &[u8]) -> Option<[u8; N]> { + let (taken, after) = rest.split_first_chunk()?; + *rest = after; + Some(*taken) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::world::block::BlockStateTable; + + #[test] + fn keys_pack_and_find_their_parents() { + for key in [ + SectionKey { + level: 0, + x: 0, + y: 0, + z: 0, + }, + SectionKey { + level: 4, + x: -1, + y: -2, + z: 8_000_000, + }, + SectionKey { + level: 2, + x: 123_456, + y: 127, + z: -654_321, + }, + ] { + assert_eq!(SectionKey::unpack(key.pack()), key); + } + let vanilla = SectionKey::of_vanilla(0, -1, 3, 33); + assert_eq!( + vanilla, + SectionKey { + level: 0, + x: -1, + y: 1, + z: 16 + } + ); + assert_eq!( + SectionKey::of_vanilla(4, -1, 3, 33), + SectionKey { + level: 4, + x: -1, + y: 0, + z: 1 + } + ); + assert_eq!(vanilla.child_index(), 1 | 1 << 2, "x odd, z even, y odd"); + assert_eq!( + SectionKey { + level: 1, + x: 1, + y: 0, + z: 0 + } + .origin(), + [64, 0, 0] + ); + } + + fn states() -> States { + States::new(BlockStateTable::native()) + } + + fn state_id(state: &str) -> u32 { + u32::from(BlockStateTable::native().find_state(state, &[]).id()) + } + + /// A vanilla section of `state` everywhere, lit `light`. + fn uniform(state: &str, light: u8) -> VoxelizedSection { + let table = BlockStateTable::native(); + let mut section = pomme_protocol::section::Section::default(); + let id = state_id(state); + for i in 0..4096u32 { + section + .states + .get_and_set(i & 15, i >> 8, i >> 4 & 15, id, table.state_count()); + } + let mut voxelized = VoxelizedSection::new(3, -2, 5); + voxelized.convert(&states(), §ion, |_, _, _| light); + voxelized + } + + #[test] + fn a_section_converts_and_mips_down_to_one_voxel() { + let mut stone = uniform("stone", 0x0F); + assert_eq!(stone.non_air_count, 4096); + stone.mip(&states()); + for level in 0..LEVELS { + let voxels = stone.level(level); + assert_eq!(voxels.len(), 4096 >> (3 * level)); + assert!( + voxels + .iter() + .all(|&v| voxel::block_id(v) == state_id("stone")) + ); + } + let mut air = uniform("cave_air", 0x3F); + assert_eq!(air.non_air_count, 0); + air.mip(&states()); + assert!( + air.level(4) + .iter() + .all(|&v| voxel::is_air(v) && voxel::light(v) == 0x3F) + ); + } + + #[test] + fn insertion_lands_each_level_in_its_sub_cube() { + let mut stone = uniform("stone", 0); + stone.mip(&states()); + // Where vanilla section (3, -2, 5)'s sub-cube starts at each level. + let corners = [ + [16, 0, 16], + [24, 16, 8], + [12, 24, 20], + [6, 28, 10], + [3, 30, 5], + ]; + for (level, [x, y, z]) in (0..LEVELS).zip(corners) { + let mut into = WorldSection::unseen(); + let (changed, before) = insert_level(&stone, &mut into, level); + assert!(changed); + assert_eq!(before, 0); + let side = level_side(level); + let filled = into.data.iter().filter(|&&v| !voxel::is_air(v)).count(); + assert_eq!(filled, side.pow(3)); + assert!(!voxel::is_air(into.data[index(x, y, z)])); + assert!(voxel::is_air(into.data[index(x + side, y, z)])); + let (changed, before) = insert_level(&stone, &mut into, level); + assert!(!changed); + assert_eq!(before, side.pow(3) as u32); + } + } + + #[test] + fn emptiness_bubbles_up() { + let mut level0 = WorldSection::unseen(); + assert_eq!(settle_level(&mut level0, 0, true, 0, 10, None), (true, 2)); + assert_eq!(level0.non_empty_children, 0xFF); + assert_eq!(settle_level(&mut level0, 0, true, 10, 4, None), (true, 0)); + let mut parent = WorldSection::unseen(); + assert_eq!( + settle_level(&mut parent, 1, false, 0, 0, Some((5, true))), + (true, 2) + ); + assert_eq!(parent.non_empty_children, 1 << 5); + assert_eq!( + settle_level(&mut parent, 1, false, 0, 0, Some((2, true))), + (true, 1) + ); + assert_eq!( + settle_level(&mut parent, 1, false, 0, 0, Some((2, true))), + (false, 0) + ); + } + + #[test] + fn sections_survive_a_file() { + let key = SectionKey { + level: 0, + x: -5, + y: 2, + z: 9, + }; + let mut section = WorldSection::unseen(); + section.data[index(1, 2, 3)] = voxel::compose(0x12, 77, 3); + section.data[index(31, 31, 31)] = voxel::compose(0, 78, 3); + section.non_empty_count = 2; + section.non_empty_children = 0xFF; + let read = decode(key, &encode(key, §ion)).unwrap(); + assert_eq!(read.data, section.data); + assert_eq!(read.non_empty_children, 0xFF); + assert_eq!(read.non_empty_count, 2); + assert!(decode(SectionKey { level: 1, ..key }, &encode(key, §ion)).is_none()); + assert!(decode(key, b"not a section").is_none()); + } + + #[test] + fn damaged_files_read_as_nothing() { + let key = SectionKey { + level: 0, + x: 1, + y: 2, + z: 3, + }; + let mut section = WorldSection::unseen(); + section.data[0] = voxel::compose(0, 5, 0); + let file = encode(key, §ion); + assert!(decode(key, &file[..file.len() / 2]).is_none(), "truncated"); + + let zlib = |bytes: &[u8]| { + let mut zlib = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::fast()); + zlib.write_all(bytes).unwrap(); + zlib.finish().unwrap() + }; + let mut raw = Vec::new(); + flate2::read::ZlibDecoder::new(file.as_slice()) + .read_to_end(&mut raw) + .unwrap(); + // Two palette entries: an index past them, then a palette cut short. + let mut past = raw.clone(); + past[16..18].copy_from_slice(&2u16.to_le_bytes()); + assert!( + decode(key, &zlib(&past)).is_none(), + "index past the palette" + ); + let short = &raw[..raw.len() - 8]; + assert!(decode(key, &zlib(short)).is_none(), "short palette"); + assert!(decode(key, &zlib(&raw)).is_some()); + } +} diff --git a/pomme-client/src/renderer/distant/shader_consts.rs b/pomme-client/src/renderer/distant/shader_consts.rs new file mode 100644 index 000000000..5ce77ab25 --- /dev/null +++ b/pomme-client/src/renderer/distant/shader_consts.rs @@ -0,0 +1,51 @@ +// Distant terrain's sizes and markers the GPU passes share with the CPU. +// `include!`d by gpu.rs and by build.rs, which writes them into the +// shaders' generated `distant_consts.glsl`. + +/// Detail levels, a traversal iteration each (section.rs `LEVELS`). +pub const LEVELS: u32 = 5; +/// Nodes the node buffer holds, a power of two. +pub const MAX_NODES: u32 = 1 << 20; +/// A node's mesh or first child: none yet, and nothing there. +pub const NULL_ID: u32 = (1 << 24) - 1; +pub const EMPTY_ID: u32 = (1 << 24) - 2; +/// The id the cleaner's output starts filled with, past every id a node is +/// given (nodes.rs allocates below it). +pub const CLEANER_SENTINEL: u32 = MAX_NODES - 2; +/// Nodes a cleaner pass picks. +pub const CLEANER_OUTPUT: u32 = 256; +/// Requests a traversal may make a frame. +pub const MAX_REQUESTS: u32 = 50; +/// Sections a frame's render list, and nodes a traversal iteration, may hold. +pub const MAX_LISTED: u32 = 200_000; +/// Quads the shared index buffer covers, the most a draw can hold; the +/// cube's indices follow. +pub const QUADS_PER_DRAW: u32 = 1 << 16; +/// The draw list's regions, in draws: opaque, translucent, then temporal. +pub const OPAQUE_DRAWS: u32 = 400_000; +pub const TRANSLUCENT_DRAWS: u32 = 100_000; +pub const TEMPORAL_DRAWS: u32 = 100_000; +pub const TRANSLUCENT_OFFSET: u32 = OPAQUE_DRAWS; +pub const TEMPORAL_OFFSET: u32 = TRANSLUCENT_OFFSET + TRANSLUCENT_DRAWS; +/// Distance buckets the translucent draws sort by, ahead of their list. +pub const TRANSLUCENT_BUCKETS: u32 = 1024; +/// Byte offsets within the draw-count buffer: after cmdgen's dispatch size, +/// the three regions' counts and the occlusion pass's draw. +pub const COUNTS_OPAQUE: u64 = 12; +pub const COUNTS_TRANSLUCENT: u64 = 16; +pub const COUNTS_TEMPORAL: u64 = 20; +pub const COUNTS_CULL_DRAW: u64 = 24; +/// The workgroup of the passes over a list, an invocation per entry. +pub const LIST_GROUP: u32 = 128; +/// The traversal's workgroup, a power of two. +pub const TRAVERSAL_GROUP_BITS: u32 = 5; +/// The cleaner's sort: invocations a workgroup, and nodes each covers. +pub const CLEANER_SORT_THREADS: u32 = 64; +pub const CLEANER_SORT_ELEMS: u32 = 8; +/// A scatter index with this bit lands in the node buffer. +pub const NODE_SCATTER_BIT: u32 = 1 << 31; +/// A visibility-set id with this bit was allocated, without it freed. +pub const VISIBILITY_RESET_BIT: u32 = 1 << 31; +/// A section's visibility word: the frame the occlusion pass last saw it, +/// with this bit when it saw it the frame before too. +pub const SEEN_BEFORE_BIT: u32 = 1 << 31; diff --git a/pomme-client/src/renderer/distant/voxel.rs b/pomme-client/src/renderer/distant/voxel.rs new file mode 100644 index 000000000..2c930b470 --- /dev/null +++ b/pomme-client/src/renderer/distant/voxel.rs @@ -0,0 +1,270 @@ +//! Distant terrain's voxel: a `u64` of block state, biome and light; and how +//! eight of them merge into one of the next level. + +use crate::world::block::BlockStateTable; + +const BLOCK_SHIFT: u32 = 27; +const BIOME_SHIFT: u32 = 47; +const LIGHT_SHIFT: u32 = 56; +const BLOCK_MASK: u64 = ((1 << 20) - 1) << BLOCK_SHIFT; +pub(crate) const BIOME_MASK: u64 = 0x1FF << BIOME_SHIFT; +pub(crate) const LIGHT_MASK: u64 = 0xFF << LIGHT_SHIFT; +/// A fluid voxel above level 0: how many blocks of air lie between its top +/// and the fluid's surface, so every level draws the surface on one plane. +/// A departure: the reference draws a coarse fluid voxel full. +const GAP_MASK: u64 = 0xF; + +/// Block state 0 is air, whatever kind. +pub(crate) const AIR: u32 = 0; + +/// The biome field's last value, stored for every registry id from it on. +/// It has no tint row, so it draws untinted. +pub(crate) const UNKNOWN_BIOME: u32 = 0x1FF; + +/// A biome's registry id as a voxel's biome. The reference numbers biomes +/// per world as it first meets them; registry ids serve here since the +/// cache folder is kept per biome registry. Ids that don't fit share +/// [`UNKNOWN_BIOME`] rather than another biome's tint. +pub(crate) fn biome(id: u32) -> u32 { + id.min(UNKNOWN_BIOME) +} + +/// `light` is `block << 4 | sky`. Air keeps no biome. +pub(crate) const fn compose(light: u8, block: u32, biome: u32) -> u64 { + let light = (light as u64) << LIGHT_SHIFT; + if block == AIR { + return light; + } + light | (biome as u64) << BIOME_SHIFT | (block as u64) << BLOCK_SHIFT +} + +pub(crate) fn is_air(voxel: u64) -> bool { + voxel & BLOCK_MASK == 0 +} + +pub(crate) fn block_id(voxel: u64) -> u32 { + ((voxel & BLOCK_MASK) >> BLOCK_SHIFT) as u32 +} + +pub(crate) fn light(voxel: u64) -> u8 { + (voxel >> LIGHT_SHIFT) as u8 +} + +pub(crate) fn gap(voxel: u64) -> u8 { + (voxel & GAP_MASK) as u8 +} + +pub(crate) fn with_light(voxel: u64, light: u8) -> u64 { + voxel & !LIGHT_MASK | u64::from(light) << LIGHT_SHIFT +} + +/// What voxelizing and mipping ask of each block state: whether it's air, +/// and its opacity, leaves counting as solid and the rest as their light +/// dampening. +pub(crate) struct States { + air: Box<[bool]>, + opacity: Box<[u8]>, + /// Fluids and blocks holding one. + fluid: Box<[bool]>, +} + +impl States { + pub fn new(table: BlockStateTable) -> Self { + let data = (0..table.state_count() as u32).map(|id| { + azalea_block::BlockState::try_from(id).map(|state| table.block_state_data(state)) + }); + let air = data + .clone() + .map(|data| data.is_ok_and(|data| data.is_air())); + let opacity = data.clone().map(|data| match data { + Ok(data) if data.is_leaves() => 15, + Ok(data) => data.light_props().dampening, + Err(_) => 0, + }); + let fluid = data.map(|data| { + data.is_ok_and(|data| { + data.is_fluid_block() || data.fluid().kind != crate::world::block::FluidKind::Empty + }) + }); + Self { + air: air.collect(), + opacity: opacity.collect(), + fluid: fluid.collect(), + } + } + + /// State `id` as a voxel's block: `AIR` for every kind of air. + pub fn block(&self, id: u32) -> u32 { + if self.air.get(id as usize).copied().unwrap_or(true) { + AIR + } else { + id + } + } + + fn is_fluid(&self, voxel: u64) -> bool { + self.fluid + .get(block_id(voxel) as usize) + .copied() + .unwrap_or(false) + } + + fn opacity(&self, voxel: u64) -> u8 { + self.opacity + .get(block_id(voxel) as usize) + .copied() + .unwrap_or(0) + } + + /// Of eight children, indexed `x << 2 | y << 1 | z`, the most opaque + /// block wins, ties going to the highest index (x, then y, then z). All + /// air: air lit by the mean of their light, block light rounded down, + /// sky up. A fluid winner records the gap down to the highest fluid + /// surface among the children, in blocks of the level-`level` voxel. + pub fn mip(&self, children: [u64; 8], level: u8) -> u64 { + let best = children + .iter() + .enumerate() + .filter(|&(_, &child)| !is_air(child)) + .map(|(i, &child)| (u32::from(self.opacity(child)) << 4 | i as u32, child)) + .max_by_key(|&(score, _)| score); + if let Some((_, winner)) = best { + if !self.is_fluid(winner) { + return winner & !GAP_MASK; + } + let lowest_gap = |upper: bool| { + children + .iter() + .enumerate() + .filter(|&(i, &child)| (i >> 1 & 1 == 1) == upper && self.is_fluid(child)) + .map(|(_, &child)| gap(child)) + .min() + }; + let gap = lowest_gap(true) + .or_else(|| lowest_gap(false).map(|gap| gap + (1 << (level - 1)))) + .unwrap_or(0); + return winner & !GAP_MASK | u64::from(gap); + } + let lights = children.map(|child| u32::from(light(child))); + let block = (lights.iter().map(|l| l & 0xF0).sum::() / 8) & 0xF0; + let sky = lights.iter().map(|l| l & 0x0F).sum::().div_ceil(8); + with_light(children[7], (block | sky) as u8) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn voxels_pack_block_biome_and_light() { + let voxel = compose(0xA5, 1234, 300); + assert_eq!(block_id(voxel), 1234); + assert_eq!(voxel >> BIOME_SHIFT & 0x1FF, 300); + assert_eq!(light(voxel), 0xA5); + assert!(!is_air(voxel)); + // Air keeps only its light. + let air = compose(0x0F, AIR, 300); + assert!(is_air(air)); + assert_eq!(air, 0x0F << LIGHT_SHIFT); + assert_eq!(light(with_light(voxel, 3)), 3); + assert_eq!(block_id(with_light(voxel, 3)), 1234); + } + + #[test] + fn biomes_past_the_field_share_the_unknown_one() { + assert_eq!(biome(300), 300); + assert_eq!(biome(600), UNKNOWN_BIOME); + let voxel = compose(0, 1, biome(600)); + assert_eq!(voxel >> BIOME_SHIFT & 0x1FF, u64::from(UNKNOWN_BIOME)); + assert_eq!(light(voxel), 0, "no spill into the light"); + } + + fn states(opacity: &[u8]) -> States { + States { + air: vec![false; opacity.len()].into(), + opacity: opacity.into(), + fluid: vec![false; 4].into(), + } + } + + #[test] + fn water_and_waterlogged_blocks_count_as_fluid() { + let table = crate::world::block::BlockStateTable::native(); + let states = States::new(table); + let id = |name: &str| u32::from(table.find_state(name, &[]).id()); + assert!(states.is_fluid(compose(0, id("water"), 0))); + assert!(states.is_fluid(compose(0, id("seagrass"), 0))); + assert!(!states.is_fluid(compose(0, id("stone"), 0))); + } + + #[test] + fn a_fluid_winner_records_the_gap_down_to_its_surface() { + // States: 1 water, 2 stone. + let mut states = states(&[0, 1, 15, 0]); + states.fluid = vec![false, true, false, false].into(); + let (water, air, stone) = (compose(0, 1, 0), compose(0xF, AIR, 0), compose(0, 2, 0)); + // Water fills the lower half: the surface is a block down at level 1. + let mut children = [air; 8]; + for lower in [0, 1, 4, 5] { + children[lower] = water; + } + let half_full = states.mip(children, 1); + assert_eq!(block_id(half_full), 1); + assert_eq!(gap(half_full), 1); + // At level 2 an upper child at its surface wins over a lower one. + let mut children = [air; 8]; + children[0] = half_full; + children[2] = water; + assert_eq!(gap(states.mip(children, 2)), 0); + children[2] = air; + assert_eq!(gap(states.mip(children, 2)), 2 + 1); + // Stone over water: no gap. + children[7] = stone; + assert_eq!(states.mip(children, 2), stone); + // At level 4 the deepest surface, a lower child's 7 under its top, + // is 15 down: the most the gap holds. + let mut children = [air; 8]; + children[0] = water | 7; + let deep = states.mip(children, 4); + assert_eq!(gap(deep), 15); + assert_eq!(block_id(deep), 1); + } + + #[test] + fn the_most_opaque_child_wins_and_ties_go_to_the_top_corner() { + // States: 1 glass (0), 2 stone (15), 3 leaves (15). + let states = states(&[0, 0, 15, 15]); + let (glass, stone, leaves) = (compose(0, 1, 0), compose(0, 2, 0), compose(0, 3, 0)); + let mut children = [0; 8]; + children[0] = stone; + children[7] = glass; + assert_eq!(states.mip(children, 1), stone, "stone beats glass"); + children[3] = leaves; + assert_eq!( + states.mip(children, 1), + leaves, + "a tie goes to the higher corner" + ); + assert_eq!( + states.mip([0, 0, 0, 0, 0, 0, 0, glass], 1), + glass, + "any block beats air" + ); + } + + #[test] + fn all_air_children_average_their_light() { + let states = states(&[0]); + // Block light 0x10 x 4 and 0: mean 8 floors to 0; sky 1 x 3 ceils to 1. + let mut children = [0u64; 8]; + for (i, child) in children.iter_mut().enumerate() { + let block = if i < 4 { 0x10 } else { 0 }; + let sky = u8::from(i < 3); + *child = compose(block | sky, AIR, 0); + } + assert_eq!(light(states.mip(children, 1)), 0x01); + // Full sky everywhere stays full. + assert_eq!(light(states.mip([compose(0x0F, AIR, 0); 8], 1)), 0x0F); + } +} diff --git a/pomme-client/src/renderer/distant/worker.rs b/pomme-client/src/renderer/distant/worker.rs new file mode 100644 index 000000000..f74350dc7 --- /dev/null +++ b/pomme-client/src/renderer/distant/worker.rs @@ -0,0 +1,875 @@ +//! The distant terrain thread: turns chunks the server forgets into distant +//! sections, keeps them per dimension, on disk too, and meshes the sections +//! the renderer asks for and keeps them up to date. + +use std::collections::{HashMap, HashSet, VecDeque}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::thread::JoinHandle; +use std::time::{Duration, Instant}; + +use crossbeam_channel::{Receiver, Sender}; +use parking_lot::{Mutex, RwLock}; + +use super::build::{BuiltSection, Factory}; +use super::colors::DistantColors; +use super::models::{ModelFactory, ModelTile}; +use super::section::{SectionKey, WorldSection}; +use super::voxel::States; +use super::world::{Changed, World, ingest_order}; +use crate::position::ChunkPos; +use crate::world::chunk::{Chunk, ChunkLightData, ChunkStore}; + +/// Sections meshed between checks for new jobs. +const MESHES_PER_BATCH: usize = 32; +/// How far the player moves, in blocks, before the thread hears of it. +const VIEW_STEP: f64 = 32.0; +/// How often changed sections are written to disk. +const SAVE_EVERY: Duration = Duration::from_secs(60); +/// How often they're written while the thread takes in the last columns on +/// its way out, so an exit that cuts it short loses little. +const SAVE_EVERY_STOPPING: Duration = Duration::from_secs(1); +/// How long a section a capture changed waits for more before it's meshed +/// again, so a burst of captures meshes it once. +const SETTLE: Duration = Duration::from_millis(250); +/// How long the thread takes in captures before it meshes a batch, so a +/// join's flood of chunks doesn't hold up the meshes the traversal is +/// waiting on. The reference ingests and meshes on separate services; this +/// thread does both, a slice of each in turn. +const CAPTURE_SLICE: Duration = Duration::from_millis(8); +/// The sides a change reaches ([`super::world::Changed::sides`]), as +/// neighbour offsets. +const SIDES: [[i32; 3]; 6] = [ + [0, -1, 0], + [0, 1, 0], + [-1, 0, 0], + [1, 0, 0], + [0, 0, -1], + [0, 0, 1], +]; + +/// The threads of games left, still writing what they kept. +static STOPPING: Mutex>> = Mutex::new(Vec::new()); + +/// A chunk to keep, before it goes. +struct Capture { + pos: ChunkPos, + chunk: Arc>, + light: Option>, + min_y: i32, + /// The cache folder it was taken under ([`DistantTerrain::generation`]). + generation: u32, +} + +/// Captures waiting for the thread, at most one per column: a newer one +/// replaces the one queued, keeping its place. +#[derive(Default)] +struct CaptureQueue { + order: VecDeque<(Arc, ChunkPos)>, + latest: HashMap<(Arc, ChunkPos), Capture>, +} + +impl CaptureQueue { + fn push(&mut self, dimension: Arc, capture: Capture) { + let column = (dimension, capture.pos); + if self.latest.insert(column.clone(), capture).is_none() { + self.order.push_back(column); + } + } + + fn pop(&mut self) -> Option<(Arc, Capture)> { + let column = self.order.pop_front()?; + let capture = self.latest.remove(&column)?; + Some((column.0, capture)) + } + + /// Puts back a capture taken off, unless a newer one of its column came + /// since. + fn requeue(&mut self, dimension: Arc, capture: Capture) { + if !self + .latest + .contains_key(&(Arc::clone(&dimension), capture.pos)) + { + self.push(dimension, capture); + } + } + + fn is_empty(&self) -> bool { + self.order.is_empty() + } + + fn clear(&mut self) { + self.order.clear(); + self.latest.clear(); + } +} + +/// The next capture, the queue's lock let go before it's taken in. +fn next(captures: &Mutex) -> Option<(Arc, Capture)> { + captures.lock().pop() +} + +/// Everything else the game tells the thread, taken before any capture +/// since the renderer's requests are what's on screen. +enum Job { + Colors(Arc), + View(View), + /// Sections the renderer wants meshed, and kept meshed as they change. + Request(Vec), + /// Sections the renderer dropped. + Dropped(Vec), + /// Where sections are kept on disk, a folder per dimension; `None` + /// keeps them in memory only. + Cache { + dir: Option, + generation: u32, + }, + /// Writes what's unsaved. + Flush, + /// Writes what's unsaved and ends the thread. + Stop, +} + +#[derive(Clone)] +struct View { + dimension: Arc, + x: f64, + z: f64, + /// In blocks. + radius: f64, +} + +/// Newly meshed sections of one dimension, the models new ones are the +/// first to use, and the biome tints when they changed. +pub(crate) struct DistantUpdate { + pub dimension: Arc, + pub sections: Vec, + pub tiles: Vec, + pub tints: Option>, +} + +/// The game's handle on the thread. Dropping it lets the thread finish +/// writing in the background ([`wait_for_stopped`]). +pub(crate) struct DistantTerrain { + jobs: Sender, + captures: Arc>, + /// Wakes the thread for a capture. + wake: Sender<()>, + updates: Receiver, + thread: Option>, + /// The last view sent, so small moves don't flood the thread. + view: Option, + /// This server's or world's folder of distant terrain. + cache_root: Option, + /// The folder last sent, under `cache_root`. + cache: Option, + /// Counts the folders switched from, so captures taken under one never + /// land in the next. + generation: u32, +} + +impl DistantTerrain { + pub fn new(colors: Arc, cache_root: Option) -> Self { + let (jobs, job_rx) = crossbeam_channel::unbounded(); + let (wake, wake_rx) = crossbeam_channel::bounded(1); + let (update_tx, updates) = crossbeam_channel::unbounded(); + let captures = Arc::new(Mutex::new(CaptureQueue::default())); + let queue = Arc::clone(&captures); + // Games left may still be writing to the same folders. + let earlier = std::mem::take(&mut *STOPPING.lock()); + let in_memory = cache_root.is_none(); + let thread = std::thread::Builder::new() + .name("distant-terrain".into()) + .spawn(move || { + for thread in earlier { + let _ = thread.join(); + } + Worker::new(colors, update_tx, queue, in_memory).run(&job_rx, &wake_rx); + }) + .expect("failed to spawn the distant terrain thread"); + Self { + jobs, + captures, + wake, + updates, + thread: Some(thread), + view: None, + cache_root, + cache: None, + generation: 0, + } + } + + /// Keeps sections under the cache root, in a folder for the block + /// states of table `slot` and the server's `biomes`, since the voxels + /// store both by id. Captures still queued under another folder are + /// dropped rather than written to it: the table they were read with may + /// already be the next one's (the colours switch first), and the game + /// captures its columns again under the new folder as they load. + pub fn set_cache_key( + &mut self, + slot: usize, + biomes: impl IntoIterator, + ) { + let mut hash = sha1_smol::Sha1::new(); + for biome in biomes { + hash.update(biome.to_string().as_bytes()); + hash.update(&[0]); + } + let digest = hash.digest().to_string(); + let cache = self + .cache_root + .as_ref() + .map(|root| root.join(format!("{slot}-{}", &digest[..16]))); + if cache == self.cache { + return; + } + if self.cache.is_some() { + self.generation += 1; + self.captures.lock().clear(); + } + self.cache.clone_from(&cache); + let _ = self.jobs.send(Job::Cache { + dir: cache, + generation: self.generation, + }); + } + + /// Whether chunks are being kept: a view with a radius was set. + pub fn enabled(&self) -> bool { + self.view.is_some() + } + + /// Keeps `pos`'s column of `chunks` as distant terrain of `dimension`: + /// once lit, and again before it goes. + pub fn capture(&self, dimension: &str, pos: ChunkPos, chunks: &ChunkStore) { + if !self.enabled() { + return; + } + let Some(chunk) = chunks.get_chunk(&pos) else { + return; + }; + self.captures.lock().push( + dimension.into(), + Capture { + pos, + chunk, + light: chunks.light_data.get(&(pos.x, pos.z)).cloned(), + min_y: chunks.min_y(), + generation: self.generation, + }, + ); + let _ = self.wake.try_send(()); + } + + pub fn set_colors(&self, colors: Arc) { + let _ = self.jobs.send(Job::Colors(colors)); + } + + /// Sections the renderer wants, and those it dropped. + pub fn request(&self, wanted: Vec, dropped: Vec) { + if !wanted.is_empty() { + let _ = self.jobs.send(Job::Request(wanted)); + } + if !dropped.is_empty() { + let _ = self.jobs.send(Job::Dropped(dropped)); + } + } + + /// Where the player is and how far to keep, in chunks; 0 stops keeping + /// new chunks. Returns whether this turned distant terrain on, so the + /// caller can hand over the columns lit while it was off. + pub fn set_view(&mut self, dimension: &str, x: f64, z: f64, chunks: u32) -> bool { + if chunks == 0 { + self.view = None; + return false; + } + let turned_on = self.view.is_none(); + let view = View { + dimension: dimension.into(), + x, + z, + radius: f64::from(chunks * 16), + }; + let unchanged = self.view.as_ref().is_some_and(|last| { + last.dimension == view.dimension + && last.radius == view.radius + && (last.x - x).hypot(last.z - z) < VIEW_STEP + }); + if !unchanged { + self.view = Some(view.clone()); + let _ = self.jobs.send(Job::View(view)); + } + turned_on + } + + /// Has the thread, once it reaches this among its jobs, take in every + /// capture queued and write everything unsaved, for when the app may + /// be ended without dropping the game (Android in the background). + /// Returns at once. + pub fn flush(&self) { + let _ = self.jobs.send(Job::Flush); + } + + pub fn drain(&self) -> impl Iterator + '_ { + self.updates.try_iter() + } +} + +/// The thread goes on taking in the columns kept on the way out and writing +/// them, off the game's thread: the next game's thread waits for it before +/// reading, and the app waits for it before exiting ([`wait_for_stopped`]). +impl Drop for DistantTerrain { + fn drop(&mut self) { + let _ = self.jobs.send(Job::Stop); + let _ = self.wake.try_send(()); + if let Some(thread) = self.thread.take() { + STOPPING.lock().push(thread); + } + } +} + +/// Waits up to `timeout` for the threads of games left to finish writing, +/// for the app to call once its window is gone. Each file is written whole, +/// so a thread cut off loses only what it hadn't written yet. +pub(crate) fn wait_for_stopped(timeout: Duration) { + let deadline = Instant::now() + timeout; + let threads = std::mem::take(&mut *STOPPING.lock()); + for thread in threads { + while !thread.is_finished() && Instant::now() < deadline { + std::thread::sleep(Duration::from_millis(10)); + } + if !thread.is_finished() { + tracing::warn!("Exiting before distant terrain finished saving"); + return; + } + let _ = thread.join(); + } +} + +/// `name` as a single folder name: anything but letters, digits, `-`, `_` +/// and `.` becomes `_`. +pub(crate) fn folder_name(name: &str) -> String { + name.chars() + .map(|c| { + if c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.') { + c + } else { + '_' + } + }) + .collect() +} + +/// Where `dimension`'s sections go under `cache`, versioned with the file +/// format. +fn dimension_dir(cache: Option<&Path>, dimension: &str) -> Option { + Some(cache?.join(folder_name(dimension)).join("sections-v2")) +} + +/// How long the thread may sleep: not at all with captures queued, else +/// until the next section is ready to mesh or the next save is due. +fn sleep_for(queued: bool, next_ready: Option, last_save: Instant) -> Duration { + if queued { + return Duration::ZERO; + } + match next_ready { + Some(ready) => ready.saturating_duration_since(Instant::now()), + None => SAVE_EVERY.saturating_sub(last_save.elapsed()), + } +} + +/// Takes queued captures through `take` for up to `slice`; whether `take` +/// asked to stop. +fn take_slice( + captures: &Mutex, + slice: Duration, + mut take: impl FnMut(Arc, Capture) -> bool, +) -> bool { + let start = Instant::now(); + while let Some((dimension, capture)) = next(captures) { + if take(dimension, capture) { + return true; + } + if start.elapsed() >= slice { + break; + } + } + false +} + +/// The thread's kept sections: a world per dimension and where they're +/// written. +struct Store { + worlds: HashMap, World>, + cache: Option, + /// Whether there's no cache root, so no folder will ever come. + in_memory: bool, + /// `cache`'s [`DistantTerrain::generation`]. + generation: u32, + last_save: Instant, +} + +impl Store { + fn new(in_memory: bool) -> Self { + Self { + worlds: HashMap::new(), + cache: None, + in_memory, + generation: 0, + last_save: Instant::now(), + } + } + + /// `dimension`'s world, made with its folder under the cache, or kept + /// in memory only when no folder will come. + fn world(&mut self, dimension: &Arc) -> &mut World { + let (cache, in_memory) = (self.cache.as_deref(), self.in_memory); + self.worlds.entry(Arc::clone(dimension)).or_insert_with(|| { + if in_memory { + World::in_memory() + } else { + World::new(dimension_dir(cache, dimension)) + } + }) + } + + /// Writes a chunk into its dimension's sections; `None` for a capture + /// from an earlier folder, dropped. + fn ingest( + &mut self, + states: &States, + dimension: &Arc, + capture: Capture, + ) -> Option> { + if capture.generation != self.generation { + return None; + } + // Copied so the game's thread can write the chunk while it's taken + // in, which may read sections from disk. + let sections = capture.chunk.read().sections.clone(); + Some(self.world(dimension).ingest_chunk( + states, + capture.pos, + §ions, + capture.light.as_deref(), + capture.min_y, + )) + } + + fn save(&mut self) { + for world in self.worlds.values_mut() { + world.flush(); + } + self.last_save = Instant::now(); + } + + /// Keeps sections in `dir` from now on. Returns whether the sections + /// held were another folder's, written there and let go. + fn set_cache(&mut self, dir: Option, generation: u32) -> bool { + let switched = self.cache.is_some(); + if switched { + self.save(); + self.worlds.clear(); + } else { + for (dimension, world) in &mut self.worlds { + world.set_dir(dimension_dir(dir.as_deref(), dimension)); + } + } + self.cache = dir; + self.generation = generation; + switched + } + + /// Writes and lets go of every dimension's sections but `dimension`'s, + /// keeping those with nowhere to be written yet. + fn keep_only(&mut self, dimension: &str) { + self.worlds.retain(|kept, world| { + if **kept == *dimension { + return true; + } + world.flush(); + !world.is_saved() + }); + } + + /// Takes in every queued capture, in an order that reads each section + /// about once however many there are, then writes what's unsaved, + /// writing as it goes too. A capture under a folder not switched to yet + /// goes back in the queue. + fn take_all(&mut self, states: &States, captures: &Mutex) { + let mut backlog: Vec<_> = std::iter::from_fn(|| next(captures)).collect(); + backlog.sort_by_key(|(_, capture)| ingest_order(capture.pos)); + for (dimension, capture) in backlog { + if capture.generation > self.generation { + captures.lock().requeue(dimension, capture); + continue; + } + self.ingest(states, &dimension, capture); + if self.last_save.elapsed() >= SAVE_EVERY_STOPPING { + self.save(); + } + } + self.save(); + } +} + +struct Worker { + colors: Arc, + models: ModelFactory, + states: States, + factory: Factory, + updates: Sender, + captures: Arc>, + store: Store, + view: Option, + /// Sections to mesh, each from when. + dirty: HashMap, + /// Sections the renderer holds, kept up to date as captures change them. + meshed: HashSet, + /// Whether the renderer has the current colours' biome tints. + tints_sent: bool, +} + +impl Worker { + fn new( + colors: Arc, + updates: Sender, + captures: Arc>, + in_memory: bool, + ) -> Self { + Self { + states: States::new(colors.table()), + colors, + models: ModelFactory::new(), + factory: Factory::new(), + updates, + captures, + store: Store::new(in_memory), + view: None, + dirty: HashMap::new(), + meshed: HashSet::new(), + tints_sent: false, + } + } + + fn run(mut self, jobs: &Receiver, wake: &Receiver<()>) { + let captures = Arc::clone(&self.captures); + loop { + let wait = sleep_for( + !captures.lock().is_empty(), + self.dirty.values().min().copied(), + self.store.last_save, + ); + // Sleeps until there's something to do: a job, a capture, a + // section ready to mesh or a save due. + let stop = crossbeam_channel::select! { + recv(jobs) -> job => self.handle(job.ok()), + recv(wake) -> _ => false, + default(wait) => false, + }; + if stop || self.take_jobs(jobs) { + break; + } + let stop = take_slice(&captures, CAPTURE_SLICE, |dimension, capture| { + // A capture under a newer folder was queued after the job + // switching to it. + let stop = capture.generation != self.store.generation && self.take_jobs(jobs); + self.capture(&dimension, capture); + stop + }); + if stop { + break; + } + self.mesh_nearest(); + if self.store.last_save.elapsed() >= SAVE_EVERY { + self.store.save(); + } + } + // What's unsaved first, then the columns kept on the way out. + self.store.save(); + self.store.take_all(&self.states, &captures); + } + + /// Handles the jobs waiting; whether one was the stop. + fn take_jobs(&mut self, jobs: &Receiver) -> bool { + jobs.try_iter().any(|job| self.handle(Some(job))) + } + + /// Meshes every section the renderer holds again. + fn remesh_all(&mut self) { + let now = Instant::now(); + self.dirty.extend(self.meshed.iter().map(|&key| (key, now))); + } + + /// Writes a chunk into its dimension's sections and marks the meshed + /// sections it changed, and their neighbours across the sides the + /// change reached, for meshing again. + fn capture(&mut self, dimension: &Arc, capture: Capture) { + let Some(changed) = self.store.ingest(&self.states, dimension, capture) else { + return; + }; + if self + .view + .as_ref() + .is_none_or(|view| view.dimension != *dimension) + { + return; + } + let ready = Instant::now() + SETTLE; + for change in changed { + let neighbours = SIDES + .iter() + .enumerate() + .filter(|&(side, _)| change.sides & 1 << side != 0) + .map(|(_, &side)| change.key.offset(side)); + for key in std::iter::once(change.key).chain(neighbours) { + if self.meshed.contains(&key) { + self.dirty.entry(key).or_insert(ready); + } + } + } + } + + /// Handles `job`, `None` for the game gone; whether to stop. + fn handle(&mut self, job: Option) -> bool { + match job { + None | Some(Job::Stop) => return true, + Some(Job::Colors(colors)) => { + if !colors.same_models(&self.colors) { + // Ids start over; the new tiles overwrite the old. + self.models = ModelFactory::new(); + self.states = States::new(colors.table()); + } + self.colors = colors; + self.tints_sent = false; + self.remesh_all(); + } + Some(Job::View(view)) => { + if self + .view + .as_ref() + .is_none_or(|last| last.dimension != view.dimension) + { + // The renderer drops the old dimension's meshes itself. + self.meshed.clear(); + self.dirty.clear(); + } + self.store.keep_only(&view.dimension); + self.view = Some(view); + } + Some(Job::Request(keys)) => { + let now = Instant::now(); + for key in keys { + self.meshed.insert(key); + self.dirty.insert(key, now); + } + } + Some(Job::Dropped(keys)) => { + for key in keys { + self.meshed.remove(&key); + self.dirty.remove(&key); + } + } + Some(Job::Cache { dir, generation }) => { + if self.store.set_cache(dir, generation) { + // What the renderer holds, meshed again from the new + // folder. + self.remesh_all(); + } + } + Some(Job::Flush) => self.store.take_all(&self.states, &self.captures), + } + false + } + + /// Meshes the ready sections nearest the player, a batch at a time. + fn mesh_nearest(&mut self) { + if self.dirty.is_empty() { + return; + } + let Some(view) = self.view.clone() else { + self.dirty.clear(); + return; + }; + let now = Instant::now(); + let mut nearest: Vec = self + .dirty + .iter() + .filter(|&(_, &ready)| ready <= now) + .map(|(&key, _)| key) + .collect(); + if nearest.is_empty() { + return; + } + nearest.sort_by(|a, b| { + a.distance(view.x, view.z) + .total_cmp(&b.distance(view.x, view.z)) + .then(b.level.cmp(&a.level)) + }); + nearest.truncate(MESHES_PER_BATCH); + let world = self.store.world(&view.dimension); + let mut sections = Vec::with_capacity(nearest.len()); + let unseen = WorldSection::unseen(); + for key in nearest { + self.dirty.remove(&key); + // The section and the six around it, read from disk if need be. + world.load(key); + for side in SIDES { + world.load(key.offset(side)); + } + let section = world.get(key).unwrap_or(&unseen); + let mut models = self.colors.baking(&mut self.models); + sections.push( + self.factory + .build(key, section, |k| world.get(k), &mut models), + ); + } + let _ = self.updates.send(DistantUpdate { + dimension: Arc::clone(&view.dimension), + sections, + tiles: self.models.take_tiles(), + tints: (!std::mem::replace(&mut self.tints_sent, true)).then(|| self.colors.tint_lut()), + }); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::renderer::distant::world::tests::{column, temp_dir}; + use crate::world::block::BlockStateTable; + + fn capture(x: i32, generation: u32) -> Capture { + Capture { + pos: ChunkPos::new(x, 0), + chunk: Arc::new(RwLock::new(Chunk::empty(1))), + light: None, + min_y: 0, + generation, + } + } + + /// A capture of a lit four-section column, stone at the bottom. + fn lit(x: i32, generation: u32) -> Capture { + let (sections, light) = column(Some(15)); + let mut chunk = Chunk::empty(sections.len()); + chunk.sections = sections.into(); + Capture { + chunk: Arc::new(RwLock::new(chunk)), + light: light.map(Arc::new), + ..capture(x, generation) + } + } + + fn states() -> States { + States::new(BlockStateTable::native()) + } + + /// The level-0 section file of column `x`'s bottom section in `dir`. + fn file(dir: &Path, x: i32) -> PathBuf { + let SectionKey { level, x, y, z } = SectionKey::of_vanilla(0, x, 0, 0); + dimension_dir(Some(dir), "overworld") + .expect("a cache dir") + .join(format!("{level}.{x}.{y}.{z}.bin")) + } + + #[test] + fn a_newer_capture_of_a_column_replaces_the_queued_one() { + let mut queue = CaptureQueue::default(); + let overworld: Arc = "overworld".into(); + queue.push(Arc::clone(&overworld), capture(1, 0)); + queue.push(Arc::clone(&overworld), capture(2, 0)); + queue.push(Arc::clone(&overworld), capture(1, 3)); + queue.push("nether".into(), capture(1, 0)); + let popped: Vec<(i32, u32)> = std::iter::from_fn(|| queue.pop()) + .map(|(_, c)| (c.pos.x, c.generation)) + .collect(); + assert_eq!(popped, [(1, 3), (2, 0), (1, 0)]); + } + + #[test] + fn captures_past_a_slice_drain_without_another_wake() { + let captures = Mutex::new(CaptureQueue::default()); + for x in 0..3 { + captures.lock().push("overworld".into(), lit(x * 2, 0)); + } + let (states, mut store) = (states(), Store::new(true)); + let mut slices = 0; + // The run loop with no wake left: it sleeps only as long as + // `sleep_for` says. + while sleep_for(!captures.lock().is_empty(), None, Instant::now()).is_zero() { + take_slice(&captures, Duration::ZERO, |dimension, capture| { + store.ingest(&states, &dimension, capture); + false + }); + slices += 1; + } + assert_eq!(slices, 3, "one capture per slice"); + let world = store.world(&"overworld".into()); + for x in 0..3 { + assert!(world.get(SectionKey::of_vanilla(0, x * 2, 0, 0)).is_some()); + } + assert!(sleep_for(false, None, Instant::now()) > Duration::ZERO); + } + + #[test] + fn a_cache_switch_writes_the_old_folder_and_drops_its_captures() { + let (old, new) = (temp_dir("old"), temp_dir("new")); + let (states, mut store) = (states(), Store::new(false)); + let overworld: Arc = "overworld".into(); + store.set_cache(Some(old.clone()), 0); + assert!(store.ingest(&states, &overworld, lit(0, 0)).is_some()); + + assert!(store.set_cache(Some(new.clone()), 1), "another folder's"); + assert!(file(&old, 0).exists(), "written to the old folder"); + assert!(store.worlds.is_empty()); + assert!(store.ingest(&states, &overworld, lit(4, 0)).is_none()); + assert!(store.ingest(&states, &overworld, lit(8, 1)).is_some()); + store.save(); + assert!(!file(&new, 4).exists() && !file(&old, 4).exists()); + assert!(file(&new, 8).exists()); + for dir in [old, new] { + std::fs::remove_dir_all(dir).unwrap(); + } + } + + #[test] + fn a_flush_takes_in_the_queued_captures_before_writing() { + let dir = temp_dir("flush"); + let (states, mut store) = (states(), Store::new(false)); + store.set_cache(Some(dir.clone()), 0); + let captures = Mutex::new(CaptureQueue::default()); + for x in [6, 0, 2] { + captures.lock().push("overworld".into(), lit(x, 0)); + } + // Queued after a switch the thread hasn't reached: kept for it. + captures.lock().push("overworld".into(), lit(10, 1)); + store.take_all(&states, &captures); + for x in [6, 0, 2] { + assert!(file(&dir, x).exists()); + } + assert!(!file(&dir, 10).exists()); + let left: Vec = std::iter::from_fn(|| next(&captures)) + .map(|(_, capture)| capture.pos.x) + .collect(); + assert_eq!(left, [10]); + std::fs::remove_dir_all(dir).unwrap(); + } + + #[test] + fn waiting_joins_a_stopping_thread_once_it_finishes() { + use std::sync::atomic::{AtomicBool, Ordering}; + let done = Arc::new(AtomicBool::new(false)); + let flag = Arc::clone(&done); + STOPPING.lock().push(std::thread::spawn(move || { + std::thread::sleep(Duration::from_millis(50)); + flag.store(true, Ordering::SeqCst); + })); + let start = Instant::now(); + wait_for_stopped(Duration::from_secs(30)); + assert!(done.load(Ordering::SeqCst)); + assert!(start.elapsed() < Duration::from_secs(5)); + assert!(STOPPING.lock().is_empty()); + } +} diff --git a/pomme-client/src/renderer/distant/world.rs b/pomme-client/src/renderer/distant/world.rs new file mode 100644 index 000000000..fda015696 --- /dev/null +++ b/pomme-client/src/renderer/distant/world.rs @@ -0,0 +1,557 @@ +//! One dimension's distant world: its sections at every level, the most +//! recently used in memory and every one seen on disk, a file per section. + +use std::collections::{BTreeMap, HashMap, HashSet}; +use std::path::PathBuf; + +use pomme_protocol::section::Section; + +use super::section::{self, LEVELS, SectionKey, VoxelizedSection, WorldSection}; +use super::voxel::States; +use crate::position::ChunkPos; +use crate::world::chunk::ChunkLightData; +use crate::world::light::DataLayer; + +/// Least-recently-used sections held in memory, the reference's cache size +/// on a small heap. Changes are written by [`World::flush`] or on eviction. +const CACHE_SIZE: usize = 1024; +/// Keys known to have no file, so meshing doesn't look for them again; +/// forgotten all at once past this many. +const ABSENT_SIZE: usize = 1 << 16; + +/// A section an update changed, and which of its sides the change touched +/// (a bit each: down, up, -x, +x, -z, +z), so the sections across them can +/// be meshed again. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct Changed { + pub key: SectionKey, + pub sides: u8, +} + +pub(crate) struct World { + /// Each with when it was last used. + sections: HashMap, + /// `sections` by last use, oldest first. + used: BTreeMap, + clock: u64, + capacity: usize, + dir: Option, + /// Sections changed since they were last written. + unsaved: HashSet, + /// Sections `dir` has no file for. + absent: HashSet, + /// Never to be given a folder, so nothing it holds waits to be written. + in_memory: bool, +} + +impl Default for World { + fn default() -> Self { + Self::in_memory() + } +} + +impl World { + pub fn new(dir: Option) -> Self { + Self { + sections: HashMap::new(), + used: BTreeMap::new(), + clock: 0, + capacity: CACHE_SIZE, + dir, + unsaved: HashSet::new(), + absent: HashSet::new(), + in_memory: false, + } + } + + pub fn in_memory() -> Self { + Self { + in_memory: true, + ..Self::new(None) + } + } + + /// Gives a world kept without a folder its folder: what it holds is + /// written there as it's evicted or flushed. + pub fn set_dir(&mut self, dir: Option) { + self.dir = dir; + self.absent.clear(); + } + + fn path(&self, key: SectionKey) -> Option { + let SectionKey { level, x, y, z } = key; + Some(self.dir.as_ref()?.join(format!("{level}.{x}.{y}.{z}.bin"))) + } + + /// Reads `key` from disk unless it's in memory already. Returns whether + /// it's in memory now. + pub fn load(&mut self, key: SectionKey) -> bool { + if let Some((_, used)) = self.sections.get_mut(&key) { + self.used.remove(&*used); + self.clock += 1; + *used = self.clock; + self.used.insert(self.clock, key); + return true; + } + if self.absent.contains(&key) { + return false; + } + let Some(path) = self.path(key) else { + return false; + }; + let read = std::fs::read(&path).ok(); + match read.as_deref().and_then(|file| section::decode(key, file)) { + Some(section) => { + self.insert(key, section); + true + } + None => { + if read.is_some() { + tracing::warn!("Ignoring unreadable distant terrain {}", path.display()); + } + if self.absent.len() >= ABSENT_SIZE { + self.absent.clear(); + } + self.absent.insert(key); + false + } + } + } + + /// `key`'s section if it's in memory. + pub fn get(&self, key: SectionKey) -> Option<&WorldSection> { + self.sections.get(&key).map(|(section, _)| section) + } + + /// Keeps `section` as the most recently used. + fn insert(&mut self, key: SectionKey, section: WorldSection) { + self.clock += 1; + self.sections.insert(key, (section, self.clock)); + self.used.insert(self.clock, key); + self.evict(); + } + + /// Drops the least recently used sections past the cache's size, + /// writing those unsaved first. Without a folder they go unwritten: what + /// waits for one is bounded by the cache like the rest. + fn evict(&mut self) { + while self.sections.len() > self.capacity { + let Some((_, key)) = self.used.pop_first() else { + break; + }; + if self.unsaved.remove(&key) { + self.save(key); + } + self.sections.remove(&key); + } + } + + /// `key`'s section, loaded, or unseen air where there's none. + fn acquire(&mut self, key: SectionKey) -> &mut WorldSection { + if !self.load(key) { + self.insert(key, WorldSection::unseen()); + } + &mut self + .sections + .get_mut(&key) + .expect("the newest section stays") + .0 + } + + /// Writes a vanilla section's voxels into every level, stopping at the + /// first level nothing changed in, and bubbles a section going empty or + /// non-empty up to its parent. + pub fn insert_update(&mut self, update: &VoxelizedSection) -> Vec { + let mut changed = Vec::new(); + let mut child: Option<(u8, bool)> = None; + for level in 0..LEVELS { + let key = SectionKey::of_vanilla(level, update.x, update.y, update.z); + let into = self.acquire(key); + let (written, before) = section::insert_level(update, into, level); + let (touched, emptiness) = + section::settle_level(into, level, written, before, update.non_air_count, child); + let non_empty = into.non_empty_children != 0; + if touched { + self.absent.remove(&key); + self.unsaved.insert(key); + changed.push(Changed { + key, + sides: if written { + neighbour_sides(update, level) + } else { + 0 + }, + }); + } + if !written && emptiness != 2 { + break; + } + child = (emptiness == 2).then_some((key.child_index(), non_empty)); + } + changed + } + + /// Takes in a column of `sections` once any of them has light: every + /// one converted, mipped and written in. A column without light is + /// taken only when it's all air, as unlit air, so a column that went + /// before its light arrived never darkens what was kept lit. Returns + /// what changed. + pub fn ingest_chunk( + &mut self, + states: &States, + pos: ChunkPos, + sections: &[Section], + light: Option<&ChunkLightData>, + min_y: i32, + ) -> Vec { + let light = light.filter(|light| has_light(light, sections.len())); + if light.is_none() && sections.iter().any(|section| section.block_count != 0) { + return Vec::new(); + } + let mut changed = Vec::new(); + for (i, section) in sections.iter().enumerate() { + let section_y = i as i32 + min_y.div_euclid(16); + let mut update = VoxelizedSection::new(pos.x, section_y, pos.z); + match light { + Some(light) => { + update.convert(states, section, section::light_of(light, section_y)); + update.mip(states); + } + None => update.zero(), + } + changed.extend(self.insert_update(&update)); + } + changed + } + + /// Writes `key`'s section, or removes its file once nothing is left in + /// it: a missing section reads as unseen. + fn save(&self, key: SectionKey) { + let (Some(path), Some(section)) = (self.path(key), self.get(key)) else { + return; + }; + if section.is_unseen() { + let _ = std::fs::remove_file(&path); + return; + } + let written = std::fs::create_dir_all(path.parent().expect("a section file has a folder")) + .and_then(|()| crate::util::write_atomic(&path, §ion::encode(key, section))); + if let Err(error) = written { + tracing::warn!("Failed to save distant terrain {}: {error}", path.display()); + } + } + + /// Writes every section changed since it was last written; without a + /// folder they wait for one. + pub fn flush(&mut self) { + if self.dir.is_none() { + return; + } + for key in std::mem::take(&mut self.unsaved) { + self.save(key); + } + } + + /// Whether dropping the world would lose nothing. + pub fn is_saved(&self) -> bool { + self.in_memory || self.unsaved.is_empty() + } +} + +/// A column's place in a Z-order curve over x and z. Columns taken in in +/// this order fill each section before moving on, so a backlog larger than +/// the cache reads each section from disk about once. +pub(crate) fn ingest_order(pos: ChunkPos) -> u64 { + let spread = |c: i32| { + let mut v = u64::from(c as u32 ^ 0x8000_0000); + v = (v | v << 16) & 0x0000_FFFF_0000_FFFF; + v = (v | v << 8) & 0x00FF_00FF_00FF_00FF; + v = (v | v << 4) & 0x0F0F_0F0F_0F0F_0F0F; + v = (v | v << 2) & 0x3333_3333_3333_3333; + (v | v << 1) & 0x5555_5555_5555_5555 + }; + spread(pos.x) | spread(pos.z) << 1 +} + +/// Whether any of a column's `sections` has light data, sky or block: the +/// reference's `LIGHT_AND_DATA`. Layer 0 is the section below the world. +fn has_light(light: &ChunkLightData, sections: usize) -> bool { + let any = + |layers: &[Option]| layers.iter().skip(1).take(sections).any(Option::is_some); + any(&light.sky_sections) || any(&light.block_sections) +} + +/// Which sides of the level-`level` section a vanilla section's update +/// reaches: those it borders on. +fn neighbour_sides(update: &VoxelizedSection, level: u8) -> u8 { + let shift = level + 1; + let crosses = |c: i32, step: i32| u8::from((c ^ (c + step)) >> shift != 0); + crosses(update.y, -1) + | crosses(update.y, 1) << 1 + | crosses(update.x, -1) << 2 + | crosses(update.x, 1) << 3 + | crosses(update.z, -1) << 4 + | crosses(update.z, 1) << 5 +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::renderer::distant::voxel; + use crate::world::block::BlockStateTable; + + /// A vanilla section at `x`/`y`/`z` of `block` everywhere, mipped. + fn stone(x: i32, y: i32, z: i32, block: u32) -> VoxelizedSection { + let mut section = VoxelizedSection::new(x, y, z); + section.fill_for_test( + voxel::compose(0, block, 1), + if block == voxel::AIR { 0 } else { 4096 }, + ); + section + } + + /// A fresh folder under the temp dir for test `name`. + pub(crate) fn temp_dir(name: &str) -> PathBuf { + let dir = std::env::temp_dir().join(format!("pomme-distant-{name}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + dir + } + + #[test] + fn an_update_reaches_every_level_and_stops_once_nothing_changes() { + let mut world = World::default(); + let changed = world.insert_update(&stone(1, 0, 0, 7)); + assert_eq!(changed.len(), LEVELS as usize); + // Level 0 holds vanilla sections 0..1 per axis, so from (1, 0, 0) + // the update reaches the -y, +x and -z sides. + assert_eq!(changed[0].sides, 1 | 1 << 3 | 1 << 4); + let level4 = SectionKey { + level: 4, + x: 0, + y: 0, + z: 0, + }; + let section = world.get(level4).unwrap(); + assert!(!voxel::is_air(section.data[section::index(1, 0, 0)])); + assert_eq!(section.non_empty_children, 1); + let level0 = SectionKey { level: 0, ..level4 }; + assert_eq!(world.get(level0).unwrap().non_empty_count, 4096); + // The same again changes nothing past level 0. + assert!(world.insert_update(&stone(1, 0, 0, 7)).is_empty()); + // Clearing it empties every level. + let cleared = world.insert_update(&stone(1, 0, 0, voxel::AIR)); + assert_eq!(cleared.len(), LEVELS as usize); + assert_eq!(world.get(level4).unwrap().non_empty_children, 0); + } + + #[test] + fn neighbour_sides_hold_for_negative_sections() { + // Level 0 holds vanilla sections -2..-1 per axis: from -1 an update + // reaches the +x side, from -2 the -x side. + let sides = |x: i32, level: u8| neighbour_sides(&VoxelizedSection::new(x, 0, 0), level); + let x_sides = |sides: u8| sides >> 2 & 0b11; + assert_eq!(x_sides(sides(-1, 0)), 0b10); + assert_eq!(x_sides(sides(-2, 0)), 0b01); + // Level 1 holds -4..-1: -3 and -2 reach neither side. + assert_eq!(x_sides(sides(-3, 1)), 0); + assert_eq!(x_sides(sides(-2, 1)), 0); + assert_eq!(x_sides(sides(-4, 1)), 0b01); + // Section y 0 still borders -y (on y -1) and not +y. + assert_eq!(sides(-1, 0) & 0b11, 0b01); + } + + #[test] + fn evicted_sections_come_back_from_disk() { + let dir = temp_dir("evict"); + let mut world = World::new(Some(dir.clone())); + world.capacity = 1; + world.insert_update(&stone(100, 0, 0, 7)); + assert_eq!(world.sections.len(), 1, "the rest written and dropped"); + world.flush(); + + let mut world = World::new(Some(dir.clone())); + world.insert_update(&stone(101, 0, 0, 8)); + let level4 = world + .get(SectionKey { + level: 4, + x: 3, + y: 0, + z: 0, + }) + .unwrap(); + assert_eq!(voxel::block_id(level4.data[section::index(4, 0, 0)]), 7); + assert_eq!(voxel::block_id(level4.data[section::index(5, 0, 0)]), 8); + assert!(!world.load(SectionKey { + level: 0, + x: -9, + y: 0, + z: 0 + })); + // A section cleared back to unseen loses its file. + let level0 = SectionKey::of_vanilla(0, 103, 0, 0); + world.insert_update(&stone(103, 0, 0, 9)); + world.flush(); + assert!(world.path(level0).unwrap().exists()); + let mut cleared = stone(103, 0, 0, voxel::AIR); + cleared.fill_for_test(voxel::compose(0x0F, voxel::AIR, 0), 0); + world.insert_update(&cleared); + world.flush(); + assert!(!world.path(level0).unwrap().exists()); + std::fs::remove_dir_all(dir).unwrap(); + } + + #[test] + fn memory_holds_the_most_recently_used_and_absent_keys_stay_bounded() { + let dir = temp_dir("lru"); + let mut world = World::new(Some(dir.clone())); + world.capacity = 8; + for x in 0..10 { + world.insert_update(&stone(x * 64, 0, 0, 7)); + assert!(world.sections.len() <= 8); + assert_eq!(world.used.len(), world.sections.len()); + } + // The last update's five levels are the newest. + for level in 0..LEVELS { + assert!( + world + .get(SectionKey::of_vanilla(level, 9 * 64, 0, 0)) + .is_some() + ); + } + // The first, dropped, was written on the way out. + let first = SectionKey::of_vanilla(0, 0, 0, 0); + assert!(world.get(first).is_none()); + assert!(world.path(first).unwrap().exists()); + assert!(world.load(first)); + + let missing = |x| SectionKey { + level: 0, + x, + y: 50, + z: 0, + }; + world.absent.extend((0..ABSENT_SIZE as i32).map(missing)); + assert!(!world.load(missing(-1))); + assert_eq!(world.absent.len(), 1, "forgotten, then the new one kept"); + std::fs::remove_dir_all(dir).unwrap(); + } + + #[test] + fn a_world_without_a_folder_coming_stays_bounded() { + let mut world = World { + capacity: 8, + ..World::in_memory() + }; + for x in 0..10 { + world.insert_update(&stone(x * 64, 0, 0, 7)); + } + assert_eq!(world.sections.len(), 8); + assert!(world.is_saved(), "nothing to wait for"); + } + + #[test] + fn z_order_keeps_neighbouring_columns_together() { + let order = |x, z| ingest_order(ChunkPos::new(x, z)); + assert!(order(-1, -1) < order(0, -1)); + assert!(order(0, -1) < order(-1, 0)); + // The four columns of a 2x2 block come one after another. + let mut block = [order(0, 0), order(1, 0), order(0, 1), order(1, 1)]; + block.sort_unstable(); + assert_eq!(block[3] - block[0], 3); + } + + #[test] + fn a_world_waiting_for_a_folder_stays_bounded() { + let mut world = World { + capacity: 8, + ..World::new(None) + }; + for x in 0..10 { + world.insert_update(&stone(x * 64, 0, 0, 7)); + } + assert_eq!(world.sections.len(), 8); + assert_eq!(world.unsaved.len(), 8, "the dropped ones went unwritten"); + } + + #[test] + fn sections_kept_before_a_folder_are_written_once_there_is_one() { + let mut world = World::new(None); + world.insert_update(&stone(5, 0, 0, 7)); + world.flush(); + assert_eq!(world.sections.len(), LEVELS as usize, "nothing dropped"); + assert!(!world.is_saved()); + + let dir = temp_dir("late-dir"); + world.set_dir(Some(dir.clone())); + world.flush(); + assert!(world.is_saved()); + let mut reread = World::new(Some(dir.clone())); + for level in 0..LEVELS { + assert!(reread.load(SectionKey::of_vanilla(level, 5, 0, 0))); + } + std::fs::remove_dir_all(dir).unwrap(); + } + + /// A column of four sections, the lowest stone, lit `light` if any. + pub(crate) fn column(light: Option) -> (Vec
, Option) { + let table = BlockStateTable::native(); + let id = u32::from(table.find_state("stone", &[]).id()); + let mut sections = vec![Section::default(); 4]; + for i in 0..4096u32 { + sections[0] + .states + .get_and_set(i & 15, i >> 8, i >> 4 & 15, id, table.state_count()); + } + sections[0].block_count = 4096; + let light = light.map(|level| ChunkLightData { + sky_sections: vec![Some(DataLayer::filled(level)); 6], + block_sections: vec![None; 6], + min_y: 0, + has_sky: true, + sky_top_section: Some(6), + }); + (sections, light) + } + + #[test] + fn a_column_without_light_keeps_what_was_kept_lit() { + let states = States::new(BlockStateTable::native()); + let mut world = World::default(); + let pos = ChunkPos::new(0, 0); + let (sections, light) = column(Some(9)); + assert!( + !world + .ingest_chunk(&states, pos, §ions, light.as_ref(), 0) + .is_empty() + ); + let key = SectionKey::of_vanilla(0, 0, 1, 0); + let lit = world.get(key).unwrap().data.clone(); + assert_eq!(voxel::light(lit[section::index(0, 16, 0)]), 9); + + // The same column gone before its light: nothing taken. + let (sections, _) = column(None); + assert!( + world + .ingest_chunk(&states, pos, §ions, None, 0) + .is_empty() + ); + let no_layers = ChunkLightData { + sky_sections: vec![None; 6], + sky_top_section: None, + ..light.unwrap() + }; + assert!( + world + .ingest_chunk(&states, pos, §ions, Some(&no_layers), 0) + .is_empty() + ); + assert_eq!(world.get(key).unwrap().data, lit); + + // An all-air column without light is taken, as unlit air. + let air = vec![Section::default(); 4]; + assert!(!world.ingest_chunk(&states, pos, &air, None, 0).is_empty()); + let section = world.get(key).unwrap(); + assert!(voxel::is_air(section.data[section::index(0, 0, 0)])); + assert_eq!(voxel::light(section.data[section::index(0, 16, 0)]), 0); + } +} diff --git a/pomme-client/src/renderer/mod.rs b/pomme-client/src/renderer/mod.rs index 5876d381a..645b63f52 100644 --- a/pomme-client/src/renderer/mod.rs +++ b/pomme-client/src/renderer/mod.rs @@ -2,6 +2,7 @@ pub mod block_entity_model; pub mod boot; pub mod camera; pub mod chunk; +pub(crate) mod distant; pub mod entity_model; pub(crate) mod fog; pub(crate) mod packing; @@ -12,6 +13,11 @@ pub(crate) mod shader; pub(crate) mod util; pub(crate) const MAX_FRAMES_IN_FLIGHT: usize = 3; + +/// The overworld's ambient haze end (`fog_end_distance`), which distant +/// terrain scales up to its own distance. +const AMBIENT_HAZE_END: f32 = 1024.0; + /// iOS keeps a backgrounded app's view and Metal layer, so its surface /// outlives a suspend; Android takes the native window away. const KEEPS_SURFACE_WHEN_SUSPENDED: bool = cfg!(target_os = "ios"); @@ -213,6 +219,8 @@ enum RenderMode<'a> { weather: &'a [WeatherColumn], cloud_mode: CloudMode, render_distance: u32, + /// How far distant terrain draws, in chunks; 0 for none. + distant_terrain: u32, player_preview: Option, book_preview: Option, eyes_in_water: bool, @@ -235,6 +243,12 @@ pub struct MenuSkin { #[derive(Default, Clone)] pub struct RenderTimings { + /// The GPU's own time per pass, from the frame whose fence signalled + /// last: what finished before each named stamp. + pub gpu: Vec<(&'static str, f32)>, + /// Chunk sections and triangles the last cull drew. + pub sections: u32, + pub triangles: u32, pub pace_ms: f32, /// Encoding a world frame, the shader pack's passes included. pub frame_ms: f32, @@ -296,7 +310,10 @@ pub struct Renderer { /// What the selected pack reads about the player's surroundings. pack_world: pipelines::shaderpack::PackWorld, chunk_buffers: ChunkBufferStore, + distant_buffers: distant::gpu::DistantBuffers, + distant_pipeline: pipelines::distant::DistantPipeline, frames: rhi::backend::Frames, + gpu_timer: Option, screenshot: screenshot::ScreenshotCapture, swapchain_dirty: bool, /// The native window is gone (Android backgrounded the app), and with it @@ -494,6 +511,14 @@ impl Renderer { ); let chunk_buffers = ChunkBufferStore::new(&ctx.device, ctx.indirect_draw); + let distant_buffers = distant::gpu::DistantBuffers::new(&ctx.device, ctx.indirect_draw); + let distant_pipeline = pipelines::distant::DistantPipeline::new( + &ctx.device, + swapchain_state.targets(), + &lightmap, + &distant_buffers, + ctx.indirect_draw.fragment_stores, + ); let mut item_entity_pipeline = pipelines::item_entity::ItemEntityPipeline::new( &ctx.device, @@ -530,6 +555,7 @@ impl Renderer { &atlas.uv_map, &render_assets, ); + let gpu_timer = rhi::backend::GpuTimer::new(&ctx, 16); Ok(Self { ctx, @@ -569,7 +595,10 @@ impl Renderer { gui_item_atlas, font_options, chunk_buffers, + distant_buffers, + distant_pipeline, frames, + gpu_timer, screenshot: screenshot::ScreenshotCapture::new(game_dir.to_path_buf()), swapchain_dirty: false, suspended: false, @@ -1019,6 +1048,8 @@ impl Renderer { self.height, self.vsync, )?; + // Its targets read the old swapchain's depth. + self.distant_pipeline.drop_targets(&self.ctx.device); // Adopt the swapchain's actual extent (may differ from the requested // window size, e.g. macOS fullscreen) so the viewport, menu layout, blur, @@ -1034,6 +1065,7 @@ impl Renderer { self.chunk_pipeline.destroy(&self.ctx.device); self.chunk_pipeline = ChunkPipeline::new(&self.ctx.device, targets, &self.atlas, &self.lightmap); + self.distant_pipeline.rebuild(&self.ctx.device, targets); self.hand_pipeline .recreate_pipeline(&self.ctx.device, targets); @@ -1112,6 +1144,17 @@ impl Renderer { &self.last_timings } + fn gpu_stamp( + &mut self, + encoder: &rhi::backend::CommandEncoder, + frame: usize, + what: &'static str, + ) { + if let Some(timer) = &mut self.gpu_timer { + timer.stamp(encoder, frame, what); + } + } + pub fn update_camera( &mut self, input: &mut InputState, @@ -1379,8 +1422,10 @@ impl Renderer { } /// Push the CPU visibility graph's per-column visible-section masks to the - /// chunk buffer store, which omits occluded sections from the GPU cull. - pub fn set_chunk_visibility(&mut self, vis: HashMap) { + /// chunk buffer store, which omits occluded sections from the GPU cull, + /// and to the distant bounds, which follow what it draws. + pub fn set_chunk_visibility(&mut self, vis: Arc>) { + self.distant_buffers.set_visibility(&self.ctx.device, &vis); self.chunk_buffers.set_chunk_visibility(vis); } @@ -1410,7 +1455,8 @@ impl Renderer { self.frames.wait_all(&self.ctx.device); } - /// Upload a batch of chunk meshes in a single coalesced transfer. + /// Upload as many of a batch of chunk meshes as fit the frame's staging + /// buffer, in a single coalesced transfer. pub fn upload_chunk_meshes(&mut self, meshes: &[ChunkMeshData]) -> chunk::buffer::ChunkUploads { self.chunk_buffers.set_eye(self.camera_render_position()); self.chunk_buffers.upload_batch(&self.ctx.device, meshes) @@ -1425,6 +1471,76 @@ impl Renderer { self.chunk_buffers.clear(); } + /// Newly meshed distant terrain sections + /// ([`distant::gpu::DistantBuffers::upload`]). + pub(crate) fn upload_distant( + &mut self, + updates: impl IntoIterator, + ) { + self.distant_buffers.upload(updates); + } + + /// The distant sections the last frame's traversal asked for, and those + /// it dropped ([`distant::gpu::DistantBuffers::take_requests`]). + pub(crate) fn take_distant_requests( + &mut self, + ) -> ( + Vec, + Vec, + ) { + self.distant_buffers.take_requests() + } + + /// Where distant terrain draws from and how far; `None` for not at all. + pub(crate) fn set_distant_view(&mut self, view: Option) { + self.distant_buffers.set_view(view); + } + + /// Which of column `pos`'s sections draw (a bit per section from + /// `min_section` up), where distant terrain gives way to them. + pub fn set_distant_column_sections(&mut self, pos: ChunkPos, sections: u32, min_section: i32) { + self.distant_buffers + .set_column_sections(&self.ctx.device, pos, sections, min_section); + } + + /// Drops all distant terrain and forgets every loaded chunk, for a new + /// level. + pub fn reset_distant(&mut self) { + self.wait_for_all_frames(); + self.distant_buffers.clear(); + self.distant_buffers.clear_bounds(); + } + + /// Whether distant terrain is uploaded at block column `x`/`z`. + #[cfg(feature = "test-harness")] + pub fn has_distant_column(&self, x: i32, z: i32) -> bool { + self.distant_buffers.has_column(x, z) + } + + /// Whether distant terrain out to `chunks` draws this frame: on, and + /// the vanilla terrain path drawing (not a shader pack's, nor the + /// top-down benchmark view). + fn distant_draws(&self, chunks: u32) -> bool { + chunks > 0 + && self.shader_pack.is_none() + && self.chunk_buffers.format() == TerrainFormat::Vanilla + && self.camera.top_down().is_none() + } + + /// The render distance the fog bands use, in chunks, and the scale of + /// the ambient haze: pushed out to distant terrain's distance while it + /// draws, so real chunks run into it without a wall of fog. + fn distant_fog(&self, render_distance: u32, distant_terrain: u32) -> (u32, f32) { + if !self.distant_draws(distant_terrain) { + return (render_distance, 1.0); + } + let blocks = (distant_terrain * 16) as f32; + ( + render_distance.max(distant_terrain), + (blocks / AMBIENT_HAZE_END).max(1.0), + ) + } + /// The block ids to mesh terrain for: the shader pack's while one can /// draw the world, else none for vanilla. pub fn pack_block_ids(&self) -> Option> { @@ -1481,6 +1597,7 @@ impl Renderer { weather: &[WeatherColumn], cloud_mode: CloudMode, render_distance: u32, + distant_terrain: u32, player_preview: Option, book_preview: Option, eyes_in_water: bool, @@ -1539,6 +1656,7 @@ impl Renderer { weather, cloud_mode, render_distance, + distant_terrain, player_preview, book_preview, eyes_in_water, @@ -1823,10 +1941,16 @@ impl Renderer { let t_fence = std::time::Instant::now(); self.frames.wait(&self.ctx.device)?; let fence_ms = t_fence.elapsed().as_secs_f32() * 1000.0; + self.last_timings.gpu = self + .gpu_timer + .as_ref() + .map(|timer| timer.read(&self.ctx.device, frame)) + .unwrap_or_default(); // Fence signalled: reclaim chunk slices the GPU is now provably done with, // and read back any screenshot copy recorded for this frame index. self.chunk_buffers.begin_frame(&self.ctx.device); + self.distant_buffers.begin_frame(&self.ctx.device, frame); self.screenshot.collect_ready(frame, &self.ctx.device); let t_acquire = std::time::Instant::now(); @@ -1844,11 +1968,15 @@ impl Renderer { if let RenderMode::World { ref sky, render_distance, + distant_terrain, eyes_in_water, .. } = mode { - let uniform = CameraUniform::new(&self.camera, sky, render_distance, eyes_in_water); + let (fog_distance, haze_scale) = self.distant_fog(render_distance, distant_terrain); + let uniform = + CameraUniform::new(&self.camera, sky, fog_distance, eyes_in_water, haze_scale); + self.distant_pipeline.update_camera(frame, &uniform); self.chunk_pipeline.update_camera(frame, &uniform); self.block_overlay_pipeline.update_camera(frame, &uniform); self.block_outline_pipeline.update_camera(frame, &uniform); @@ -1867,8 +1995,14 @@ impl Renderer { } let encoder = self.frames.begin(&self.ctx.device)?; + if let Some(timer) = &mut self.gpu_timer { + timer.begin_frame(&encoder, frame); + } self.atlas.record_animation_upload(&encoder, frame); + self.distant_buffers.record_model_upload(&encoder, frame); + self.distant_buffers.record_uploads(&encoder, frame); self.lightmap.record(&self.ctx.device, &encoder); + self.gpu_stamp(&encoder, frame, "uploads"); let scissor = rhi::Rect::covering(self.swapchain.extent); @@ -1883,6 +2017,9 @@ impl Renderer { self.camera.anchor(), self.camera_render_position(), ); + self.gpu_stamp(&encoder, frame, "cull"); + self.last_timings.sections = self.chunk_buffers.sections_drawn(); + self.last_timings.triangles = self.chunk_buffers.triangles_drawn(); } let player_team_color = match &mode { @@ -2043,11 +2180,15 @@ impl Renderer { } let pack_drawn = self.render_pack_world(&encoder, frame, &mode); + // Depth kept past the pass: distant terrain draws against it in + // passes of its own, then the frame goes on. let mut pass = encoder.begin_render_pass( &self.ctx.device, - &self - .swapchain - .pass(image_index, rhi::LoadOp::Clear(clear_color)), + &self.swapchain.pass_keeping_depth( + image_index, + rhi::LoadOp::Clear(clear_color), + rhi::LoadOp::Clear(1.0), + ), ); let sw = self.swapchain.extent.width as f32; @@ -2078,6 +2219,7 @@ impl Renderer { weather, cloud_mode, render_distance, + distant_terrain, player_preview, book_preview, eyes_in_water, @@ -2093,16 +2235,17 @@ impl Renderer { // Vanilla water fog hides the sky dome and clouds; the framebuffer // is cleared to the water fog color, so skipping them tints the view // when looking up out of geometry. + let (fog_distance, _) = self.distant_fog(*render_distance, *distant_terrain); if !*eyes_in_water { self.sky_pipeline.update_and_draw( &mut pass, frame, &self.camera, sky, - *render_distance, + fog_distance, ); } - + self.gpu_stamp(&encoder, frame, "sky"); let t_cull = std::time::Instant::now(); // A pack that failed to build leaves its vertex format until // the next frame remeshes; skip those. @@ -2123,9 +2266,52 @@ impl Renderer { self.moving_block_buffers.draw(&pass, frame, true); } let cull_ms = t_cull.elapsed().as_secs_f32() * 1000.0; + self.gpu_stamp(&encoder, frame, "terrain"); - let anchor = self.camera.anchor(); let eye = self.camera_render_position(); + let distant = self.distant_draws(*distant_terrain); + if distant { + self.distant_buffers.update_roots(eye); + } + // Distant terrain draws after the chunks' solid and cutout + // passes: bounded by their depth, into its own image, then + // laid into the frame. Nothing to traverse means nothing + // to draw or request. + if distant && self.distant_buffers.top_count() > 0 { + let extent = self.swapchain.extent; + pass.end(); + self.distant_pipeline.render( + &self.ctx.device, + &encoder, + frame, + &mut self.distant_buffers, + &self.camera, + eye, + *render_distance, + self.swapchain.depth.handle(), + extent, + sky.cardinal_light.table(), + ); + pass = encoder.begin_render_pass( + &self.ctx.device, + &self.swapchain.pass_keeping_depth( + image_index, + rhi::LoadOp::Load, + rhi::LoadOp::Load, + ), + ); + self.distant_pipeline.composite( + &mut pass, + frame, + &self.camera, + (*render_distance * 16) as f32, + (*distant_terrain * 16) as f32, + ); + } + + self.gpu_stamp(&encoder, frame, "distant"); + + let anchor = self.camera.anchor(); if let Some((block_pos, stage, state)) = destroy_info { self.block_overlay_pipeline.draw( @@ -2185,6 +2371,8 @@ impl Renderer { .draw(&mut pass, frame, boxes, origin, screen); } + self.gpu_stamp(&encoder, frame, "entities"); + // Translucent water draws after opaque terrain and entities so it // blends over them; depth-tested (occluded by geometry in front) // but doesn't write depth. @@ -2193,6 +2381,7 @@ impl Renderer { self.chunk_buffers .draw_indirect(&pass, frame, Layer::Water, View::Camera); } + self.gpu_stamp(&encoder, frame, "water"); // Clouds draw after opaque world geometry (so terrain occludes // them) and before weather, depth-tested against the scene. @@ -2219,6 +2408,7 @@ impl Renderer { if *show_chunk_borders { self.chunk_border_pipeline.draw(&mut pass, frame); } + self.gpu_stamp(&encoder, frame, "clouds"); cull_ms }; @@ -2285,6 +2475,8 @@ impl Renderer { } } + self.gpu_stamp(&encoder, frame, "hand"); + // Vanilla blits the outline after the hand, before the GUI. if self.entity_renderer.has_outlines() || self.item_entity_pipeline.has_outlines() { pass.end(); @@ -2309,6 +2501,8 @@ impl Renderer { self.post_effects.finish(&mut pass); } + self.gpu_stamp(&encoder, frame, "post"); + let (blurred_pass, vbase, overlay) = self.draw_until_blur( &encoder, image_index, @@ -2401,6 +2595,7 @@ impl Renderer { pass.end(); self.swapchain.ready_to_present(&encoder, image_index); + self.gpu_stamp(&encoder, frame, "hud"); // The image is ready to present; grab it first if F2 was pressed. self.screenshot.record_if_armed( @@ -2415,10 +2610,13 @@ impl Renderer { self.gui_item_atlas.end_frame(); let t_present = std::time::Instant::now(); - match self - .frames - .submit_and_present(encoder, &self.ctx, &self.swapchain, image_index)? - { + let presented = + self.frames + .submit_and_present(encoder, &self.ctx, &self.swapchain, image_index)?; + if let Some(timer) = &mut self.gpu_timer { + timer.submitted(frame); + } + match presented { rhi::Presented::Done => {} rhi::Presented::OutOfDate => self.swapchain_dirty = true, rhi::Presented::Suboptimal => { @@ -2777,6 +2975,8 @@ impl Drop for Renderer { } self.chunk_buffers.destroy(&self.ctx.device); self.chunk_pipeline.destroy(&self.ctx.device); + self.distant_buffers.destroy(&self.ctx.device); + self.distant_pipeline.destroy(&self.ctx.device); self.lightmap.destroy(&self.ctx.device); self.hand_pipeline.destroy(&self.ctx.device); self.block_overlay_pipeline.destroy(&self.ctx.device); @@ -2803,6 +3003,9 @@ impl Drop for Renderer { self.gui_item_atlas.destroy(&self.ctx.device); self.atlas.destroy(&self.ctx.device); + if let Some(timer) = &mut self.gpu_timer { + timer.destroy(&self.ctx.device); + } self.frames.destroy(&self.ctx.device); self.swapchain.destroy(&self.ctx); diff --git a/pomme-client/src/renderer/pipelines/distant.rs b/pomme-client/src/renderer/pipelines/distant.rs new file mode 100644 index 000000000..2cccc0867 --- /dev/null +++ b/pomme-client/src/renderer/pipelines/distant.rs @@ -0,0 +1,1308 @@ +//! Distant terrain's passes: the drawn chunks' boxes into a depth bound; last +//! frame's draw list into the far terrain's own image under its own +//! projection where the chunks drew nothing; that depth reduced into the +//! Hi-Z the node traversal culls by as it picks this frame's sections; the +//! listed sections' boxes tested against the depth; this frame's draw list +//! built from the ones that showed and the newly seen drawn after; then the +//! image blitted into the frame with its depth carried over. + +use glam::{DVec3, Mat4}; + +use crate::renderer::camera::{Camera, CameraUniform}; +use crate::renderer::chunk::buffer::DrawCommand; +use crate::renderer::chunk::occlusion_graph::VIS_MARGIN_RADIANS; +use crate::renderer::distant::gpu::{ + CLEANER_IDS_BYTES, CLEANER_KEYS_BYTES, COUNTS_BYTES, COUNTS_CULL_DRAW, CUBE_FIRST_INDEX, + DISTANT_FAR, DRAWS_BYTES, DistantBuffers, LIST_GROUP, NODE_VISIBILITY_BYTES, POSITIONS_BYTES, + QUEUE_META_BYTES, RENDER_LIST_BYTES, REQUESTS_BYTES, Region, SCRATCH_BYTES, SECTIONS_BYTES, + TOP_IDS_BYTES, TRANSLUCENT_BUCKETS, TRANSLUCENT_BYTES, TRAVERSAL_GROUP, VISIBILITY_BYTES, + VISIBILITY_SETS_BYTES, +}; +use crate::renderer::distant::nodes::NODE_BYTES; +use crate::renderer::distant::section::LEVELS; +use crate::renderer::pipelines::lightmap::Lightmap; +use crate::renderer::rhi::TextureUse; +use crate::renderer::rhi::backend::TextureTransition; +use crate::renderer::util::{storage_binding, uniform_binding}; +use crate::renderer::{MAX_FRAMES_IN_FLIGHT, rhi, shader, util}; +use crate::world::block::model::{CardinalLighting, Direction}; + +/// Distant terrain's image: plain RGBA8, lit as the quads draw. +const FORMAT: rhi::TextureFormat = rhi::TextureFormat::Rgba8Unorm; +const DEPTH: rhi::TextureFormat = rhi::TextureFormat::Depth32Float; +/// The Hi-Z's levels: a colour texture holding the reduced depth, so each +/// level can be attached on its own and written by a full-screen pass. +const HIZ: rhi::TextureFormat = rhi::TextureFormat::R32Float; +/// A node is split while it covers more than this many pixels squared. +const SUBDIVISION_SIZE: f32 = 64.0; + +/// The scene uniform (`distant_section.glsl`): the distant view projection +/// moved by the camera's position within its 32-block section, that section, +/// the frame, whether the occlusion pass was skipped, and the traversal's +/// inputs, the frustum it draws within and the wider one it requests within. +#[repr(C)] +#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] +struct SceneUniform { + view_proj: [f32; 16], + camera_section: [i32; 3], + frame_id: u32, + camera_sub_pos: [f32; 3], + occlusion_skipped: u32, + hiz_size: u32, + min_sss: f32, + request_limit: u32, + render_distance: f32, + frustum: [[f32; 4]; 6], + request_frustum: [[f32; 4]; 6], + face_shade: [[f32; 4]; 2], +} + +// std140: the frustums from byte 112, the shades last. +const _: () = assert!(size_of::() == 336); + +const SCENE_BYTES: u64 = size_of::() as u64; +const CAMERA_BYTES: u64 = size_of::() as u64; + +/// A sampled texture and its sampler, for the fragment stage. +const TEXTURE_SAMPLER: &[rhi::BindGroupLayoutEntry] = &[ + rhi::BindGroupLayoutEntry::texture(rhi::ShaderStages::FRAGMENT), + rhi::BindGroupLayoutEntry::sampler(rhi::ShaderStages::FRAGMENT), +]; +/// The blit's inputs: the image, its depth, and the sampler. +const BLIT_INPUTS: &[rhi::BindGroupLayoutEntry] = &[ + rhi::BindGroupLayoutEntry::texture(rhi::ShaderStages::FRAGMENT), + rhi::BindGroupLayoutEntry::texture(rhi::ShaderStages::FRAGMENT), + rhi::BindGroupLayoutEntry::sampler(rhi::ShaderStages::FRAGMENT), +]; +/// The quads' set 3: each draw's section position, and the depth bound. +const DRAW_SET: &[rhi::BindGroupLayoutEntry] = &[ + rhi::BindGroupLayoutEntry::storage(rhi::ShaderStages::VERTEX), + rhi::BindGroupLayoutEntry::texture(rhi::ShaderStages::FRAGMENT), +]; +const COMPUTE_UNIFORM: rhi::BindGroupLayoutEntry = + rhi::BindGroupLayoutEntry::uniform(rhi::ShaderStages::COMPUTE); +const COMPUTE_STORAGE: rhi::BindGroupLayoutEntry = + rhi::BindGroupLayoutEntry::storage(rhi::ShaderStages::COMPUTE); +/// `distant_traversal.comp`: the scene, then the request queue, render +/// list, nodes, queue metadata, source and sink queues, the Hi-Z and the +/// nodes' last-drawn frames. +const TRAVERSAL_SET: &[rhi::BindGroupLayoutEntry] = &[ + COMPUTE_UNIFORM, + COMPUTE_STORAGE, + COMPUTE_STORAGE, + COMPUTE_STORAGE, + COMPUTE_STORAGE, + COMPUTE_STORAGE, + COMPUTE_STORAGE, + rhi::BindGroupLayoutEntry::texture(rhi::ShaderStages::COMPUTE), + rhi::BindGroupLayoutEntry::sampler(rhi::ShaderStages::COMPUTE), + COMPUTE_STORAGE, +]; +/// `distant_visibility_set.comp`: the last-drawn frames and the frame's +/// ids. +const VISIBILITY_SET_SET: &[rhi::BindGroupLayoutEntry] = &[COMPUTE_STORAGE; 2]; +/// `distant_cleaner_sort.comp`: the last-drawn frames, the output ids and +/// the nodes. +const CLEANER_SORT_SET: &[rhi::BindGroupLayoutEntry] = &[COMPUTE_STORAGE; 3]; +/// `distant_cleaner_results.comp`: the output ids, the nodes, the output +/// keys and the last-drawn frames. +const CLEANER_RESULTS_SET: &[rhi::BindGroupLayoutEntry] = &[COMPUTE_STORAGE; 4]; +/// `distant_prep.comp`: the draw counts and the render list. +const PREP_SET: &[rhi::BindGroupLayoutEntry] = &[COMPUTE_STORAGE; 2]; +/// `distant_cmdgen.comp`: the scene, then the draws, counts, sections, +/// visibility, render list, positions and translucent data. +const CMDGEN_SET: &[rhi::BindGroupLayoutEntry] = &[ + COMPUTE_UNIFORM, + COMPUTE_STORAGE, + COMPUTE_STORAGE, + COMPUTE_STORAGE, + COMPUTE_STORAGE, + COMPUTE_STORAGE, + COMPUTE_STORAGE, + COMPUTE_STORAGE, +]; +/// `distant_prefix_sum.comp`: the translucent data. +const PREFIX_SET: &[rhi::BindGroupLayoutEntry] = &[COMPUTE_STORAGE]; +/// `distant_translucents.comp`: the scene, then the draws, counts, +/// sections, render list and translucent data. +const TRANSLUCENTS_SET: &[rhi::BindGroupLayoutEntry] = &[ + COMPUTE_UNIFORM, + COMPUTE_STORAGE, + COMPUTE_STORAGE, + COMPUTE_STORAGE, + COMPUTE_STORAGE, + COMPUTE_STORAGE, +]; +/// `distant_cull.vert`/`.frag`: the scene, the sections, the visibility the +/// fragments write, and the render list. +const CULL_SET: &[rhi::BindGroupLayoutEntry] = &[ + rhi::BindGroupLayoutEntry::uniform(rhi::ShaderStages::VERTEX), + rhi::BindGroupLayoutEntry::storage(rhi::ShaderStages::VERTEX), + rhi::BindGroupLayoutEntry::storage(rhi::ShaderStages::VERTEX_FRAGMENT), + rhi::BindGroupLayoutEntry::storage(rhi::ShaderStages::VERTEX), +]; + +/// Which queues a traversal iteration reads and writes: the first walks +/// the top-level ids, then the two scratch queues alternate. +#[derive(Clone, Copy)] +enum Iteration { + First, + Odd, + Even, +} + +/// The screen-size targets, remade with the swapchain +/// ([`DistantPipeline::drop_targets`]). +struct Targets { + color: rhi::backend::Texture, + depth: rhi::backend::Texture, + /// The far side of the drawn chunks' boxes. + bound: rhi::backend::Texture, + /// The depth reduced level by level, each a power of two no larger + /// than the screen. + hiz: rhi::backend::Texture, + hiz_width: u32, + hiz_height: u32, + hiz_levels: u32, + /// Per Hi-Z level, what it reduces: the depth, then the level before. + hiz_sets: Vec, + /// Per frame in flight, a set per [`Iteration`]. + traversal_sets: Vec, + /// The draw positions with the depth bound. + draw_set: rhi::backend::BindGroup, + setup_set: rhi::backend::BindGroup, + blit_set: rhi::backend::BindGroup, +} + +impl Targets { + fn traversal_set(&self, frame: usize, iteration: Iteration) -> rhi::backend::BindGroup { + self.traversal_sets[frame * 3 + iteration as usize] + } +} + +pub struct DistantPipeline { + /// The quads, opaque then translucent, into the image. + quads: [rhi::backend::RenderPipeline; 2], + bound: rhi::backend::RenderPipeline, + setup: rhi::backend::RenderPipeline, + hiz: rhi::backend::RenderPipeline, + blit: rhi::backend::RenderPipeline, + /// The occlusion pass; `None` where the fragment stage can't write + /// buffers, and every listed section then counts as seen. + cull: Option, + traversal: rhi::backend::ComputePipeline, + quads_layout: rhi::backend::PipelineLayout, + bound_layout: rhi::backend::PipelineLayout, + setup_layout: rhi::backend::PipelineLayout, + blit_layout: rhi::backend::PipelineLayout, + cull_layout: rhi::backend::PipelineLayout, + traversal_layout: rhi::backend::PipelineLayout, + /// A vertex-stage uniform: the camera for the bound pass, the scene + /// for the quads. + camera_layout: rhi::backend::BindGroupLayout, + camera_fragment_layout: rhi::backend::BindGroupLayout, + /// A sampled texture: the setup's frame depth, each Hi-Z level's + /// source. + texture_layout: rhi::backend::BindGroupLayout, + blit_inputs_layout: rhi::backend::BindGroupLayout, + draw_layout: rhi::backend::BindGroupLayout, + cull_set_layout: rhi::backend::BindGroupLayout, + traversal_set_layout: rhi::backend::BindGroupLayout, + /// The camera for the bound pass and the blit, which fogs in the + /// fragment stage. + camera_sets: Vec, + camera_fragment_sets: Vec, + camera_buffers: Vec, + scene_sets: Vec, + scene_buffers: Vec, + cull_sets: Vec, + prep: util::ComputePass, + cmdgen: util::ComputePass, + prefix_sum: util::ComputePass, + translucents: util::ComputePass, + /// The cleaner: the nodes' last-drawn frames kept up as nodes come and + /// go, the longest-undrawn sorted out, and their keys made for the CPU. + visibility_set: util::ComputePass, + cleaner_sort: util::ComputePass, + cleaner_results: util::ComputePass, + sampler: rhi::backend::Sampler, + targets: Option, + /// Set 2 of the quads, which [`Lightmap`] owns. + lightmap_set: rhi::backend::BindGroup, +} + +impl DistantPipeline { + pub fn new( + device: &rhi::backend::Device, + swapchain: rhi::backend::PipelineTargets, + lightmap: &Lightmap, + buffers: &DistantBuffers, + fragment_stores: bool, + ) -> Self { + let layout = |entries| rhi::backend::create_bind_group_layout(device, entries); + let camera_layout = layout(util::VERTEX_UNIFORM); + let camera_fragment_layout = layout(util::VERTEX_FRAGMENT_UNIFORM); + let texture_layout = layout(TEXTURE_SAMPLER); + let blit_inputs_layout = layout(BLIT_INPUTS); + let draw_layout = layout(DRAW_SET); + let cull_set_layout = layout(CULL_SET); + let traversal_set_layout = layout(TRAVERSAL_SET); + let push = |visibility, size| Some(rhi::PushConstants { visibility, size }); + let pipeline_layout = |sets: &[rhi::backend::BindGroupLayout], push| { + rhi::backend::create_pipeline_layout(device, sets, push) + }; + let quads_layout = pipeline_layout( + &[ + camera_layout, + buffers.layout(), + lightmap.layout, + draw_layout, + ], + None, + ); + // The view projection and the render distance, once a pass. + let bound_layout = pipeline_layout( + &[camera_layout, buffers.bound_layout()], + push(rhi::ShaderStages::VERTEX, 80), + ); + let setup_layout = pipeline_layout(&[texture_layout], None); + // The inverse distant view projection, then the fade. + let blit_layout = pipeline_layout( + &[camera_fragment_layout, blit_inputs_layout], + push(rhi::ShaderStages::FRAGMENT, 80), + ); + let cull_layout = pipeline_layout(&[cull_set_layout], None); + // Its push constant is the iteration. + let traversal_layout = + pipeline_layout(&[traversal_set_layout], push(rhi::ShaderStages::COMPUTE, 4)); + let (camera_sets, camera_buffers) = util::create_uniform_sets( + device, + camera_layout, + CAMERA_BYTES, + "distant_camera_uniform", + ); + let camera_fragment_sets = camera_buffers + .iter() + .map(|buffer| { + device.create_bind_group( + camera_fragment_layout, + &[uniform_binding(0, buffer, CAMERA_BYTES)], + ) + }) + .collect(); + let (scene_sets, scene_buffers) = + util::create_uniform_sets(device, camera_layout, SCENE_BYTES, "distant_scene_uniform"); + let draw = buffers.draw_buffers(); + let scene = |frame: usize| uniform_binding(0, &scene_buffers[frame], SCENE_BYTES); + let counts = |binding| storage_binding(binding, draw.counts, COUNTS_BYTES); + let draws = |binding| storage_binding(binding, draw.draws, DRAWS_BYTES); + let sections = |binding| storage_binding(binding, draw.sections, SECTIONS_BYTES); + let visibility = |binding| storage_binding(binding, draw.visibility, VISIBILITY_BYTES); + let translucent = |binding| storage_binding(binding, draw.translucent, TRANSLUCENT_BYTES); + let render_list = |binding| storage_binding(binding, draw.render_list, RENDER_LIST_BYTES); + let nodes = |binding| storage_binding(binding, draw.nodes, NODE_BYTES); + let node_visibility = + |binding| storage_binding(binding, draw.node_visibility, NODE_VISIBILITY_BYTES); + let cleaner_ids = |binding| storage_binding(binding, draw.cleaner_ids, CLEANER_IDS_BYTES); + let cull_sets = (0..MAX_FRAMES_IN_FLIGHT) + .map(|frame| { + device.create_bind_group( + cull_set_layout, + &[scene(frame), sections(1), visibility(2), render_list(3)], + ) + }) + .collect(); + let pass = |label, shader, entries, push, frames, bindings: &dyn Fn(usize) -> Vec<_>| { + util::ComputePass::new(device, label, shader, entries, push, frames, bindings) + }; + Self { + quads: create_quad_pipelines(device, quads_layout), + bound: create_bound_pipeline(device, bound_layout), + setup: create_setup_pipeline(device, setup_layout), + hiz: create_hiz_pipeline(device, setup_layout), + blit: create_blit_pipeline(device, swapchain, blit_layout), + cull: fragment_stores.then(|| create_cull_pipeline(device, cull_layout)), + traversal: rhi::backend::create_compute_pipeline( + device, + traversal_layout, + "distant traversal", + shader::include_shader!("distant_traversal.comp"), + ), + prep: pass( + "distant prep", + shader::include_shader!("distant_prep.comp"), + PREP_SET, + None, + 1, + &|_| vec![counts(0), render_list(1)], + ), + cmdgen: pass( + "distant command generation", + shader::include_shader!("distant_cmdgen.comp"), + CMDGEN_SET, + None, + MAX_FRAMES_IN_FLIGHT, + &|frame| { + vec![ + scene(frame), + draws(1), + counts(2), + sections(3), + visibility(4), + render_list(5), + storage_binding(6, draw.positions, POSITIONS_BYTES), + translucent(7), + ] + }, + ), + prefix_sum: pass( + "distant prefix sum", + shader::include_shader!("distant_prefix_sum.comp"), + PREFIX_SET, + None, + 1, + &|_| vec![translucent(0)], + ), + translucents: pass( + "distant translucents", + shader::include_shader!("distant_translucents.comp"), + TRANSLUCENTS_SET, + None, + MAX_FRAMES_IN_FLIGHT, + &|frame| { + vec![ + scene(frame), + draws(1), + counts(2), + sections(3), + render_list(4), + translucent(5), + ] + }, + ), + // Its push constants are the count and the frame to set. + visibility_set: pass( + "distant visibility set", + shader::include_shader!("distant_visibility_set.comp"), + VISIBILITY_SET_SET, + push(rhi::ShaderStages::COMPUTE, 8), + MAX_FRAMES_IN_FLIGHT, + &|frame| { + vec![ + node_visibility(0), + storage_binding(1, buffers.visibility_sets(frame), VISIBILITY_SETS_BYTES), + ] + }, + ), + cleaner_sort: pass( + "distant cleaner sort", + shader::include_shader!("distant_cleaner_sort.comp"), + CLEANER_SORT_SET, + None, + 1, + &|_| vec![node_visibility(0), cleaner_ids(1), nodes(2)], + ), + // Its push constant is the frame. + cleaner_results: pass( + "distant cleaner results", + shader::include_shader!("distant_cleaner_results.comp"), + CLEANER_RESULTS_SET, + push(rhi::ShaderStages::COMPUTE, 4), + 1, + &|_| { + vec![ + cleaner_ids(0), + nodes(1), + storage_binding(2, draw.cleaner_keys, CLEANER_KEYS_BYTES), + node_visibility(3), + ] + }, + ), + quads_layout, + bound_layout, + setup_layout, + blit_layout, + cull_layout, + traversal_layout, + camera_layout, + camera_fragment_layout, + texture_layout, + blit_inputs_layout, + draw_layout, + cull_set_layout, + traversal_set_layout, + camera_sets, + camera_fragment_sets, + camera_buffers, + scene_sets, + scene_buffers, + cull_sets, + sampler: rhi::backend::create_sampler(device, &rhi::SamplerDesc::NEAREST), + targets: None, + lightmap_set: lightmap.group, + } + } + + /// The swapchain's formats changed: the blit draws into it. The targets + /// go with [`Self::drop_targets`], which every swapchain rebuild calls. + pub fn rebuild( + &mut self, + device: &rhi::backend::Device, + swapchain: rhi::backend::PipelineTargets, + ) { + self.blit.destroy(device); + self.blit = create_blit_pipeline(device, swapchain, self.blit_layout); + } + + /// The swapchain was remade, its depth with it even at the same size: + /// the targets, sized to it and reading that depth, are remade by the + /// next frame that draws. + pub fn drop_targets(&mut self, device: &rhi::backend::Device) { + if let Some(targets) = self.targets.take() { + self.destroy_targets(device, targets); + } + } + + pub fn update_camera(&mut self, frame: usize, uniform: &CameraUniform) { + self.camera_buffers[frame].write(bytemuck::bytes_of(uniform)); + } + + /// The targets at `extent`, made with `frame_depth` (the swapchain's, + /// which the setup pass reads), unless they're already made. + fn ensure_targets( + &mut self, + device: &rhi::backend::Device, + extent: rhi::Extent, + frame_depth: rhi::backend::TextureHandle, + buffers: &DistantBuffers, + ) { + if self.targets.is_some() { + return; + } + let texture = |desc: rhi::TextureDesc, resting| { + let texture = rhi::backend::Texture::new(device, &desc); + device.submit(|encoder| { + encoder.transition_textures(&[TextureTransition { + texture: texture.handle(), + range: rhi::TextureRange { + mips: desc.mip_levels, + ..rhi::TextureRange::BASE + }, + from: TextureUse::Undefined, + to: resting, + }]); + }); + texture + }; + let screen = |label, format| { + rhi::TextureDesc::new( + label, + extent.width.max(1), + extent.height.max(1), + format, + rhi::TextureUsages::RENDER_ATTACHMENT | rhi::TextureUsages::TEXTURE_BINDING, + ) + }; + let color = texture(screen("distant_color", FORMAT), TextureUse::ShaderRead); + let depth = texture(screen("distant_depth", DEPTH), TextureUse::DepthRead); + let bound = texture(screen("distant_depth_bound", DEPTH), TextureUse::DepthRead); + // The Hi-Z's size: the screen rounded down to powers of two, a level + // per halving down to 2. + let highest_bit = |n: u32| 1u32 << (31 - n.max(1).leading_zeros()); + let (hiz_width, hiz_height) = (highest_bit(extent.width), highest_bit(extent.height)); + let hiz_levels = hiz_width.max(hiz_height).ilog2().max(1); + let hiz = texture( + rhi::TextureDesc { + width: hiz_width, + height: hiz_height, + mip_levels: hiz_levels, + ..screen("distant_hiz", HIZ) + }, + TextureUse::ComputeRead, + ); + let texture_set = + |source| util::create_texture_group(device, self.texture_layout, source, self.sampler); + let hiz_sets = (0..hiz_levels) + .map(|level| { + texture_set(if level == 0 { + depth.handle() + } else { + hiz.mip_handle(level - 1) + }) + }) + .collect(); + let draw = buffers.draw_buffers(); + let traversal_sets = (0..MAX_FRAMES_IN_FLIGHT) + .flat_map(|frame| { + [Iteration::First, Iteration::Odd, Iteration::Even].map(|iteration| { + let (source, source_bytes, sink) = match iteration { + Iteration::First => { + (buffers.top_ids(frame), TOP_IDS_BYTES, draw.scratch[1]) + } + Iteration::Odd => (draw.scratch[1], SCRATCH_BYTES, draw.scratch[0]), + Iteration::Even => (draw.scratch[0], SCRATCH_BYTES, draw.scratch[1]), + }; + device.create_bind_group( + self.traversal_set_layout, + &[ + uniform_binding(0, &self.scene_buffers[frame], SCENE_BYTES), + storage_binding(1, draw.requests, REQUESTS_BYTES), + storage_binding(2, draw.render_list, RENDER_LIST_BYTES), + storage_binding(3, draw.nodes, NODE_BYTES), + storage_binding(4, draw.queue_meta, QUEUE_META_BYTES), + storage_binding(5, source, source_bytes), + storage_binding(6, sink, SCRATCH_BYTES), + (7, rhi::backend::BindingResource::Texture(hiz.handle())), + (8, rhi::backend::BindingResource::Sampler(self.sampler)), + storage_binding(9, draw.node_visibility, NODE_VISIBILITY_BYTES), + ], + ) + }) + }) + .collect(); + let draw_set = device.create_bind_group( + self.draw_layout, + &[ + storage_binding(0, draw.positions, POSITIONS_BYTES), + (1, rhi::backend::BindingResource::Texture(bound.handle())), + ], + ); + let blit_set = device.create_bind_group( + self.blit_inputs_layout, + &[ + (0, rhi::backend::BindingResource::Texture(color.handle())), + (1, rhi::backend::BindingResource::Texture(depth.handle())), + (2, rhi::backend::BindingResource::Sampler(self.sampler)), + ], + ); + self.targets = Some(Targets { + setup_set: texture_set(frame_depth), + color, + depth, + bound, + hiz, + hiz_width, + hiz_height, + hiz_levels, + hiz_sets, + traversal_sets, + draw_set, + blit_set, + }); + } + + /// The frame's distant passes up to the blit, after the chunks' solid and + /// cutout passes, whose depth `frame_depth` holds: the bound pass, last + /// frame's opaque draws into the image, the Hi-Z from its depth, the + /// cleaner's passes, the traversal picking this frame's sections, the + /// occlusion pass over them, this frame's draw list, and the newly seen + /// and translucent draws, shaded by the dimension's `cardinal_lighting`. + #[allow(clippy::too_many_arguments)] + pub fn render( + &mut self, + device: &rhi::backend::Device, + encoder: &rhi::backend::CommandEncoder, + frame: usize, + buffers: &mut DistantBuffers, + camera: &Camera, + eye: DVec3, + render_distance: u32, + frame_depth: rhi::backend::TextureHandle, + extent: rhi::Extent, + cardinal_lighting: CardinalLighting, + ) { + self.ensure_targets(device, extent, frame_depth, buffers); + let targets = self.targets.as_ref().expect("made above"); + let (color, depth, bound) = ( + targets.color.handle(), + targets.depth.handle(), + targets.bound.handle(), + ); + let view_proj = camera.distant_view_projection(DISTANT_FAR); + // The scene uniform: the camera's 32-block section, and the + // projection moved by where it is within it. + let camera_section = (eye / 32.0).floor(); + let sub_pos = (eye - camera_section * 32.0).as_vec3(); + let frame_id = buffers.frame_id(); + self.scene_buffers[frame].write(bytemuck::bytes_of(&SceneUniform { + view_proj: (view_proj * Mat4::from_translation(-sub_pos)).to_cols_array(), + camera_section: camera_section.as_ivec3().to_array(), + frame_id, + camera_sub_pos: sub_pos.to_array(), + occlusion_skipped: u32::from(self.cull.is_none()), + hiz_size: targets.hiz_width << 16 | targets.hiz_height, + min_sss: SUBDIVISION_SIZE * SUBDIVISION_SIZE / (extent.width * extent.height) as f32, + request_limit: buffers.request_limit(), + render_distance: buffers.render_distance_sq(), + frustum: Camera::planes_from_view_projection(view_proj), + // Terrain beside the view refines before a turn shows it, by the + // chunk mesher's margin. A departure: the reference requests only + // what it draws. + request_frustum: Camera::planes_from_view_projection( + camera.distant_view_projection_dilated(DISTANT_FAR, VIS_MARGIN_RADIANS), + ), + face_shade: face_shade(cardinal_lighting), + })); + let draw = buffers.draw_buffers(); + let (counts, translucent, queue_meta) = ( + draw.counts.handle(), + draw.translucent.handle(), + draw.queue_meta.handle(), + ); + let indices = buffers.index_buffer(); + + // The bound pass: the farthest back face of each drawn section's box + // within the render distance, from a depth cleared to the near plane. + encoder.transition_textures(&[TextureTransition::color( + bound, + TextureUse::DepthRead, + TextureUse::DepthAttachment, + )]); + let mut pass = encoder.begin_render_pass( + device, + &rhi::backend::RenderPassDesc { + color: None, + extra_colors: Vec::new(), + depth: Some(depth_attachment(bound, rhi::LoadOp::Clear(0.0))), + extent, + }, + ); + let bounds = buffers.bounds(frame); + if bounds.count() > 0 { + pass.set_pipeline(&self.bound); + pass.set_bind_groups(0, &[self.camera_sets[frame], bounds.group(frame)]); + let mut push = view_proj.to_cols_array().to_vec(); + push.extend([(render_distance * 16) as f32, 0.0, 0.0, 0.0]); + pass.set_push_constants(rhi::ShaderStages::VERTEX, 0, bytemuck::cast_slice(&push)); + pass.set_index_buffer(indices); + pass.draw_indexed(36, bounds.count(), CUBE_FIRST_INDEX, 0, 0); + } + pass.end(); + encoder.transition_textures(&[ + TextureTransition::color(bound, TextureUse::DepthAttachment, TextureUse::DepthRead), + TextureTransition::color( + frame_depth, + TextureUse::DepthAttachment, + TextureUse::DepthRead, + ), + TextureTransition::color(color, TextureUse::ShaderRead, TextureUse::ColorAttachment), + TextureTransition::color(depth, TextureUse::DepthRead, TextureUse::DepthAttachment), + ]); + + // The image's depth set up from the chunks', then the opaque draws of + // the draw list built last frame. + let mut pass = self.begin_image_pass(device, encoder, targets, extent, true); + pass.set_pipeline(&self.setup); + pass.set_bind_group(0, targets.setup_set); + pass.draw(3, 1, 0, 0); + self.bind_quads(&mut pass, frame, targets, false); + buffers.draw(&pass, Region::Opaque); + pass.end(); + + // The Hi-Z from the depth so far, the cleaner, then the traversal. + encoder.transition_textures(&[TextureTransition::color( + depth, + TextureUse::DepthAttachment, + TextureUse::DepthRead, + )]); + self.build_hiz(device, encoder, targets); + self.clean(encoder, frame, frame_id, buffers); + let top_count = buffers.record_traversal_setup(encoder, frame); + self.traverse(encoder, frame, targets, top_count, queue_meta); + buffers.record_request_readback(encoder, frame); + encoder.transition_textures(&[TextureTransition::color( + depth, + TextureUse::DepthRead, + TextureUse::DepthAttachment, + )]); + + // `distant_prep.comp`, after last frame's draws are done with the + // counts and positions and the traversal with the render list; the + // occlusion pass's vertex stage reads that list after it. + encoder.transition_buffers_many(&[ + (rhi::BufferUse::DrawInput, rhi::BufferUse::Storage), + (rhi::BufferUse::ShaderRead, rhi::BufferUse::Storage), + (rhi::BufferUse::Storage, rhi::BufferUse::CopyDst), + ]); + encoder.dispatch(&self.prep.pipeline, self.prep.set(frame), &[], [1, 1, 1]); + encoder.fill_buffer(translucent, 0, u64::from(TRANSLUCENT_BUCKETS) * 4, 0); + buffers.record_draw_slots(encoder); + encoder.transition_buffers_many(&[ + (rhi::BufferUse::StorageWrite, rhi::BufferUse::DrawInput), + (rhi::BufferUse::StorageWrite, rhi::BufferUse::Storage), + (rhi::BufferUse::StorageWrite, rhi::BufferUse::ShaderRead), + (rhi::BufferUse::CopyDst, rhi::BufferUse::Storage), + ]); + + // The occlusion pass: each listed section's box against the image's + // depth, the ones that show marked seen this frame. + if let Some(cull) = &self.cull { + let mut pass = encoder.begin_render_pass( + device, + &rhi::backend::RenderPassDesc { + color: None, + extra_colors: Vec::new(), + depth: Some(depth_attachment(depth, rhi::LoadOp::Load)), + extent, + }, + ); + pass.set_pipeline(cull); + pass.set_bind_group(0, self.cull_sets[frame]); + pass.set_index_buffer(indices); + pass.draw_indexed_indirect( + counts, + COUNTS_CULL_DRAW, + 1, + size_of::() as u32, + ); + pass.end(); + encoder.transition_buffers(rhi::BufferUse::FragmentWrite, rhi::BufferUse::Storage); + } + + // `distant_cmdgen.comp`, then the translucent draws sorted far to + // near. + encoder.dispatch_indirect( + &self.cmdgen.pipeline, + self.cmdgen.set(frame), + &[], + counts, + 0, + ); + encoder.transition_buffers(rhi::BufferUse::StorageWrite, rhi::BufferUse::Storage); + encoder.dispatch( + &self.prefix_sum.pipeline, + self.prefix_sum.set(frame), + &[], + [1, 1, 1], + ); + encoder.transition_buffers(rhi::BufferUse::StorageWrite, rhi::BufferUse::Storage); + encoder.dispatch_indirect( + &self.translucents.pipeline, + self.translucents.set(frame), + &[], + counts, + 0, + ); + buffers.finish_list(encoder, frame); + encoder.transition_buffers_many(&[ + (rhi::BufferUse::StorageWrite, rhi::BufferUse::DrawInput), + (rhi::BufferUse::StorageWrite, rhi::BufferUse::ShaderRead), + ]); + + // The sections seen this frame but not last, then the translucent + // quads. + let mut pass = self.begin_image_pass(device, encoder, targets, extent, false); + self.bind_quads(&mut pass, frame, targets, false); + buffers.draw(&pass, Region::Temporal); + self.bind_quads(&mut pass, frame, targets, true); + buffers.draw(&pass, Region::Translucent); + pass.end(); + encoder.transition_textures(&[ + TextureTransition::color( + frame_depth, + TextureUse::DepthRead, + TextureUse::DepthAttachment, + ), + TextureTransition::color(color, TextureUse::ColorAttachment, TextureUse::ShaderRead), + TextureTransition::color(depth, TextureUse::DepthAttachment, TextureUse::DepthRead), + ]); + } + + /// The cleaner's passes: the nodes allocated and freed since last frame + /// set in the last-drawn frames; then, when the arena is nearly full, the + /// longest-undrawn nodes sorted out and their keys read back for the CPU + /// to drop the meshes of. + fn clean( + &self, + encoder: &rhi::backend::CommandEncoder, + frame: usize, + frame_id: u32, + buffers: &mut DistantBuffers, + ) { + let sets = buffers.write_visibility_sets(frame); + if sets > 0 { + encoder.dispatch( + &self.visibility_set.pipeline, + self.visibility_set.set(frame), + bytemuck::bytes_of(&[sets, frame_id]), + [sets.div_ceil(LIST_GROUP), 1, 1], + ); + encoder.transition_buffers(rhi::BufferUse::StorageWrite, rhi::BufferUse::Storage); + } + if let Some(groups) = buffers.record_cleaner_setup(encoder, frame) { + encoder.dispatch( + &self.cleaner_sort.pipeline, + self.cleaner_sort.set(0), + &[], + [groups, 1, 1], + ); + encoder.transition_buffers(rhi::BufferUse::StorageWrite, rhi::BufferUse::Storage); + encoder.dispatch( + &self.cleaner_results.pipeline, + self.cleaner_results.set(0), + &frame_id.to_le_bytes(), + [1, 1, 1], + ); + buffers.record_cleaner_readback(encoder, frame); + } + } + + /// The Hi-Z's levels, each a full-screen pass over the one before, the + /// first over the image's depth. + fn build_hiz( + &self, + device: &rhi::backend::Device, + encoder: &rhi::backend::CommandEncoder, + targets: &Targets, + ) { + let transition = |first_mip, mips, from, to| TextureTransition { + texture: targets.hiz.handle(), + range: rhi::TextureRange { + first_mip, + mips, + ..rhi::TextureRange::BASE + }, + from, + to, + }; + encoder.transition_textures(&[transition( + 0, + targets.hiz_levels, + TextureUse::ComputeRead, + TextureUse::ColorAttachment, + )]); + for level in 0..targets.hiz_levels { + let mut pass = encoder.begin_render_pass( + device, + &rhi::backend::RenderPassDesc { + color: Some(rhi::backend::ColorAttachment { + texture: targets.hiz.mip_handle(level), + format: rhi::backend::texture_format(HIZ), + load: rhi::LoadOp::Clear([0.0; 4]), + store: rhi::StoreOp::Store, + }), + extra_colors: Vec::new(), + depth: None, + extent: rhi::Extent { + width: (targets.hiz_width >> level).max(1), + height: (targets.hiz_height >> level).max(1), + }, + }, + ); + pass.set_pipeline(&self.hiz); + pass.set_bind_group(0, targets.hiz_sets[level as usize]); + pass.draw(3, 1, 0, 0); + pass.end(); + encoder.transition_textures(&[transition( + level, + 1, + TextureUse::ColorAttachment, + TextureUse::ShaderRead, + )]); + } + encoder.transition_textures(&[transition( + 0, + targets.hiz_levels, + TextureUse::ShaderRead, + TextureUse::ComputeRead, + )]); + } + + /// The node traversal: the first iteration over the top-level nodes, + /// then one per level below, each sized by the nodes the one before + /// pushed. + fn traverse( + &self, + encoder: &rhi::backend::CommandEncoder, + frame: usize, + targets: &Targets, + top_count: u32, + queue_meta: rhi::backend::BufferHandle, + ) { + if top_count > 0 { + encoder.dispatch( + &self.traversal, + targets.traversal_set(frame, Iteration::First), + &0u32.to_le_bytes(), + [top_count.div_ceil(TRAVERSAL_GROUP), 1, 1], + ); + } + for iteration in 1..u32::from(LEVELS) { + encoder.transition_buffers_many(&[ + (rhi::BufferUse::StorageWrite, rhi::BufferUse::Storage), + (rhi::BufferUse::StorageWrite, rhi::BufferUse::DrawInput), + ]); + let queues = if iteration % 2 == 1 { + Iteration::Odd + } else { + Iteration::Even + }; + encoder.dispatch_indirect( + &self.traversal, + targets.traversal_set(frame, queues), + &iteration.to_le_bytes(), + queue_meta, + u64::from(iteration) * 16, + ); + } + encoder.transition_buffers(rhi::BufferUse::StorageWrite, rhi::BufferUse::Storage); + } + + /// A pass into the image and its depth, cleared when `first`. + fn begin_image_pass( + &self, + device: &rhi::backend::Device, + encoder: &rhi::backend::CommandEncoder, + targets: &Targets, + extent: rhi::Extent, + first: bool, + ) -> rhi::backend::RenderPassEncoder { + encoder.begin_render_pass( + device, + &rhi::backend::RenderPassDesc { + color: Some(rhi::backend::ColorAttachment { + texture: targets.color.handle(), + format: rhi::backend::texture_format(FORMAT), + load: if first { + rhi::LoadOp::Clear([0.0; 4]) + } else { + rhi::LoadOp::Load + }, + store: rhi::StoreOp::Store, + }), + extra_colors: Vec::new(), + depth: Some(depth_attachment( + targets.depth.handle(), + if first { + rhi::LoadOp::Clear(1.0) + } else { + rhi::LoadOp::Load + }, + )), + extent, + }, + ) + } + + /// The quad pipeline and its sets but the arena's, which + /// [`DistantBuffers::draw`] binds. + fn bind_quads( + &self, + pass: &mut rhi::backend::RenderPassEncoder, + frame: usize, + targets: &Targets, + translucent: bool, + ) { + pass.set_pipeline(&self.quads[usize::from(translucent)]); + pass.set_bind_group(0, self.scene_sets[frame]); + pass.set_bind_group(2, self.lightmap_set); + pass.set_bind_group(3, targets.draw_set); + } + + /// The image into the frame, fogged, and faded over the last tenth of + /// `distant_blocks` (never short of `render_distance_blocks`, where the + /// chunks end). + pub fn composite( + &self, + pass: &mut rhi::backend::RenderPassEncoder, + frame: usize, + camera: &Camera, + render_distance_blocks: f32, + distant_blocks: f32, + ) { + let Some(targets) = &self.targets else { + return; + }; + let rd = distant_blocks - 32.0; + let start = render_distance_blocks.max(rd * 0.9); + let end = render_distance_blocks.max(rd); + let scale = if end > start { + 1.0 / (end - start) + } else { + 0.0 + }; + let inverse = camera.distant_view_projection(DISTANT_FAR).inverse(); + let mut constants = inverse.to_cols_array().to_vec(); + constants.extend([-start * scale, scale, 0.0, 0.0]); + pass.set_pipeline(&self.blit); + pass.set_bind_groups(0, &[self.camera_fragment_sets[frame], targets.blit_set]); + pass.set_push_constants( + rhi::ShaderStages::FRAGMENT, + 0, + bytemuck::cast_slice(&constants), + ); + pass.draw(3, 1, 0, 0); + } + + fn destroy_targets(&self, device: &rhi::backend::Device, mut targets: Targets) { + for set in targets + .hiz_sets + .drain(..) + .chain(targets.traversal_sets.drain(..)) + .chain([targets.draw_set, targets.setup_set, targets.blit_set]) + { + set.destroy(device); + } + for texture in [ + &mut targets.color, + &mut targets.depth, + &mut targets.bound, + &mut targets.hiz, + ] { + std::mem::take(texture).destroy(device); + } + } + + pub fn destroy(&mut self, device: &rhi::backend::Device) { + if let Some(targets) = self.targets.take() { + self.destroy_targets(device, targets); + } + util::destroy_buffers(device, &mut self.camera_buffers); + util::destroy_buffers(device, &mut self.scene_buffers); + util::destroy_bind_groups(device, self.camera_sets.drain(..)); + util::destroy_bind_groups(device, self.camera_fragment_sets.drain(..)); + util::destroy_bind_groups(device, self.scene_sets.drain(..)); + util::destroy_bind_groups(device, self.cull_sets.drain(..)); + for pass in [ + &mut self.prep, + &mut self.cmdgen, + &mut self.prefix_sum, + &mut self.translucents, + &mut self.visibility_set, + &mut self.cleaner_sort, + &mut self.cleaner_results, + ] { + pass.destroy(device); + } + self.traversal.destroy(device); + for pipeline in self + .quads + .iter_mut() + .chain([ + &mut self.bound, + &mut self.setup, + &mut self.hiz, + &mut self.blit, + ]) + .chain(self.cull.as_mut()) + { + pipeline.destroy(device); + } + for layout in [ + self.quads_layout, + self.bound_layout, + self.setup_layout, + self.blit_layout, + self.cull_layout, + self.traversal_layout, + ] { + layout.destroy(device); + } + for layout in [ + self.camera_layout, + self.camera_fragment_layout, + self.texture_layout, + self.blit_inputs_layout, + self.draw_layout, + self.cull_set_layout, + self.traversal_set_layout, + ] { + layout.destroy(device); + } + self.sampler.destroy(device); + } +} + +/// `distant.vert`'s shade by face, in [`Direction::VALUES`] order. +fn face_shade(lighting: CardinalLighting) -> [[f32; 4]; 2] { + let [down, up, north, south, west, east] = Direction::VALUES.map(|dir| lighting.by_face(dir)); + [[down, up, north, south], [west, east, 0.0, 0.0]] +} + +/// A `texture` loaded by `load` and kept after the pass. +fn depth_attachment( + texture: rhi::backend::TextureHandle, + load: rhi::LoadOp, +) -> rhi::backend::DepthAttachment { + rhi::backend::DepthAttachment { + texture, + load, + store: rhi::StoreOp::Store, + } +} + +/// What the quads and the setup draw into: the image and its depth. +fn image_targets() -> rhi::backend::PipelineTargets { + rhi::backend::PipelineTargets::new(rhi::backend::texture_format(FORMAT), true) +} + +/// Writing depth under `compare`. +fn depth_state(compare: rhi::CompareFunction) -> Option { + Some(rhi::DepthState { + compare, + ..rhi::DepthState::WRITE + }) +} + +/// A full-screen pass of `fragment`. +fn fullscreen_pipeline( + device: &rhi::backend::Device, + targets: rhi::backend::PipelineTargets, + layout: rhi::backend::PipelineLayout, + label: &str, + fragment: rhi::Shader, + depth: Option, + blend: Option, +) -> rhi::backend::RenderPipeline { + rhi::backend::create_render_pipeline( + device, + targets, + layout, + &rhi::RenderPipelineDesc { + depth, + blend, + ..rhi::RenderPipelineDesc::new( + label, + shader::include_shader!("blur.vert"), + fragment, + &[], + ) + }, + ) +} + +/// The opaque quads, then the translucent ones alpha-blended over them; both +/// write depth under the distant projection. Neither culls faces: the quads +/// are drawn from every side. +fn create_quad_pipelines( + device: &rhi::backend::Device, + layout: rhi::backend::PipelineLayout, +) -> [rhi::backend::RenderPipeline; 2] { + let targets = image_targets(); + let opaque = rhi::RenderPipelineDesc { + depth: Some(rhi::DepthState::WRITE), + ..rhi::RenderPipelineDesc::new( + "distant terrain", + shader::include_shader!("distant.vert"), + shader::include_shader!("distant.frag"), + &[], + ) + }; + let translucent = rhi::RenderPipelineDesc { + label: "distant translucent", + blend: Some(rhi::BlendState::TRANSLUCENT), + ..opaque + }; + [opaque, translucent] + .map(|desc| rhi::backend::create_render_pipeline(device, targets, layout, &desc)) +} + +/// The drawn chunks' boxes, back faces only and the farthest kept. +fn create_bound_pipeline( + device: &rhi::backend::Device, + layout: rhi::backend::PipelineLayout, +) -> rhi::backend::RenderPipeline { + rhi::backend::create_render_pipeline( + device, + rhi::backend::PipelineTargets::depth_only(), + layout, + &rhi::RenderPipelineDesc { + cull_mode: rhi::CullMode::Front, + depth: depth_state(rhi::CompareFunction::Greater), + ..rhi::RenderPipelineDesc::new( + "distant chunk bounds", + shader::include_shader!("distant_bound.vert"), + shader::include_shader!("distant_bound.frag"), + &[], + ) + }, + ) +} + +/// The listed sections' boxes, front faces tested against the image's +/// depth and nothing written but the visibility. +fn create_cull_pipeline( + device: &rhi::backend::Device, + layout: rhi::backend::PipelineLayout, +) -> rhi::backend::RenderPipeline { + rhi::backend::create_render_pipeline( + device, + rhi::backend::PipelineTargets::depth_only(), + layout, + &rhi::RenderPipelineDesc { + cull_mode: rhi::CullMode::Back, + depth: Some(rhi::DepthState::READ_ONLY), + ..rhi::RenderPipelineDesc::new( + "distant occlusion", + shader::include_shader!("distant_cull.vert"), + shader::include_shader!("distant_cull.frag"), + &[], + ) + }, + ) +} + +/// The image's depth set to the near plane where the chunks drew. +fn create_setup_pipeline( + device: &rhi::backend::Device, + layout: rhi::backend::PipelineLayout, +) -> rhi::backend::RenderPipeline { + fullscreen_pipeline( + device, + image_targets(), + layout, + "distant depth setup", + shader::include_shader!("distant_setup.frag"), + depth_state(rhi::CompareFunction::Always), + None, + ) +} + +/// One Hi-Z level from the one before. +fn create_hiz_pipeline( + device: &rhi::backend::Device, + layout: rhi::backend::PipelineLayout, +) -> rhi::backend::RenderPipeline { + fullscreen_pipeline( + device, + rhi::backend::PipelineTargets::new(rhi::backend::texture_format(HIZ), false), + layout, + "distant hi-z", + shader::include_shader!("distant_hiz.frag"), + None, + None, + ) +} + +/// The image over the frame, depth-tested against the chunks and written. +fn create_blit_pipeline( + device: &rhi::backend::Device, + swapchain: rhi::backend::PipelineTargets, + layout: rhi::backend::PipelineLayout, +) -> rhi::backend::RenderPipeline { + fullscreen_pipeline( + device, + swapchain, + layout, + "distant blit", + shader::include_shader!("distant_blit.frag"), + Some(rhi::DepthState::WRITE), + Some(rhi::BlendState::TRANSLUCENT), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_nether_shades_down_faces_as_up_ones() { + let [first, rest] = face_shade(CardinalLighting::NETHER); + assert_eq!(first, [0.9, 0.9, 0.8, 0.8]); + assert_eq!(rest[..2], [0.6, 0.6]); + assert_eq!( + face_shade(CardinalLighting::DEFAULT)[0], + [0.5, 1.0, 0.8, 0.8] + ); + } +} diff --git a/pomme-client/src/renderer/pipelines/mod.rs b/pomme-client/src/renderer/pipelines/mod.rs index 3663c71c1..ec62b9fde 100644 --- a/pomme-client/src/renderer/pipelines/mod.rs +++ b/pomme-client/src/renderer/pipelines/mod.rs @@ -5,6 +5,7 @@ pub mod book_preview; pub mod chunk; pub mod chunk_borders; pub mod clouds; +pub mod distant; pub mod entity_outline; pub mod entity_renderer; pub mod entity_shadow; diff --git a/pomme-client/src/renderer/rhi/mod.rs b/pomme-client/src/renderer/rhi/mod.rs index 0870a77e5..bcc0da3c9 100644 --- a/pomme-client/src/renderer/rhi/mod.rs +++ b/pomme-client/src/renderer/rhi/mod.rs @@ -120,6 +120,17 @@ pub struct BufferCopy { pub size: u64, } +impl BufferCopy { + /// The first `size` bytes of one buffer over the other's. + pub const fn from_start(size: u64) -> Self { + Self { + src_offset: 0, + dst_offset: 0, + size, + } + } +} + /// One layer of one mip level of a texture, whole, and where its texels sit /// tightly packed in a buffer: the buffer-texture copies Pomme makes. #[derive(Clone, Copy)] @@ -152,7 +163,10 @@ pub enum TextureUse { Undefined, CopySrc, CopyDst, + /// Sampled by a vertex or fragment shader. ShaderRead, + /// Sampled by a compute shader. + ComputeRead, ColorAttachment, Present, /// A depth texture as a pass's depth target. @@ -193,12 +207,18 @@ pub enum BufferUse { CopyDst, /// A compute shader's storage writes; as a destination, its reads too. StorageWrite, + /// Read or written by a compute shader. + Storage, + /// Written by a fragment shader. + FragmentWrite, + /// Read by a vertex or fragment shader. + ShaderRead, /// A draw's inputs: its indirect command, vertices and indices. DrawInput, HostRead, } -/// The optional features behind the chunk pass's indirect draws. +/// The optional features behind the GPU-driven passes. #[derive(Clone, Copy, Debug)] pub struct IndirectDrawFeatures { /// A draw count read from a buffer (`vkCmdDrawIndexedIndirectCount`, @@ -208,6 +228,10 @@ pub struct IndirectDrawFeatures { /// Several indirect draws per call (`multiDrawIndirect`). Without it each /// draw-list slot is its own call. pub multi_draw: bool, + /// Fragment shaders may write storage buffers + /// (`fragmentStoresAndAtomics`), which the distant terrain's occlusion + /// pass records its results with. + pub fragment_stores: bool, } pub enum Acquired { @@ -309,7 +333,7 @@ impl TextureDesc<'static> { extent.width, extent.height, TextureFormat::Depth32Float, - TextureUsages::RENDER_ATTACHMENT, + TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, ) } } @@ -672,6 +696,8 @@ pub enum FrontFace { pub enum CompareFunction { Less, LessEqual, + Greater, + Always, } #[derive(Clone, Copy)] diff --git a/pomme-client/src/renderer/rhi/vulkan/context.rs b/pomme-client/src/renderer/rhi/vulkan/context.rs index 87735d53a..26c39a1f6 100644 --- a/pomme-client/src/renderer/rhi/vulkan/context.rs +++ b/pomme-client/src/renderer/rhi/vulkan/context.rs @@ -206,12 +206,14 @@ impl Context { let indirect_draw = IndirectDrawFeatures { count: supports_draw_indirect_count(&physical_device), multi_draw: physical_device.get_features().multi_draw_indirect == vk::TRUE, + fragment_stores: physical_device.get_features().fragment_stores_and_atomics == vk::TRUE, }; tracing::info!("Indirect draws: {indirect_draw:?}"); let enabled_features = vk::PhysicalDeviceFeatures { multi_draw_indirect: indirect_draw.multi_draw.into(), draw_indirect_first_instance: vk::TRUE, depth_clamp: physical_device.get_features().depth_clamp, + fragment_stores_and_atomics: indirect_draw.fragment_stores.into(), ..Default::default() }; let mut vk12_features = vk::PhysicalDeviceVulkan12Features { diff --git a/pomme-client/src/renderer/rhi/vulkan/mod.rs b/pomme-client/src/renderer/rhi/vulkan/mod.rs index ee3a5f6cb..319677820 100644 --- a/pomme-client/src/renderer/rhi/vulkan/mod.rs +++ b/pomme-client/src/renderer/rhi/vulkan/mod.rs @@ -509,6 +509,9 @@ fn buffer_usage_flags(usage: BufferUsages) -> vk::BufferUsageFlags { pub struct Texture { raw: vk::Image, view: vk::ImageView, + /// A view per level when there are several, for attaching or sampling + /// one level alone ([`Self::mip_handle`]). + mip_views: Vec, allocation: Allocation, } @@ -559,9 +562,10 @@ impl Texture { return Err(error); } }; - let texture = Self { + let mut texture = Self { raw, view: vk::ImageView::null(), + mip_views: Vec::new(), allocation, }; if let Err(error) = unsafe { @@ -574,26 +578,45 @@ impl Texture { texture.destroy(device); return Err(format!("failed to bind image memory for {label}: {error}")); } - let view_info = vk::ImageViewCreateInfo { - image: raw, - view_type: match desc.dimension { - TextureViewDimension::D2 => vk::ImageViewType::Type2D, - TextureViewDimension::D2Array => vk::ImageViewType::Type2DArray, - TextureViewDimension::Cube => vk::ImageViewType::Cube, - }, - format, - subresource_range: vk::ImageSubresourceRange { - level_count: desc.mip_levels, - layer_count: desc.layers, - ..subresource_range(desc.format) - }, - ..Default::default() + let view = |first_mip, mips| { + let info = vk::ImageViewCreateInfo { + image: raw, + view_type: match desc.dimension { + TextureViewDimension::D2 => vk::ImageViewType::Type2D, + TextureViewDimension::D2Array => vk::ImageViewType::Type2DArray, + TextureViewDimension::Cube => vk::ImageViewType::Cube, + }, + format, + subresource_range: vk::ImageSubresourceRange { + base_mip_level: first_mip, + level_count: mips, + layer_count: desc.layers, + ..subresource_range(desc.format) + }, + ..Default::default() + }; + device + .raw + .create_image_view(&info, None) + .map_err(|error| format!("failed to create image view for {label}: {error}")) }; - match device.raw.create_image_view(&view_info, None) { - Ok(view) => Ok(Self { view, ..texture }), + let views = view(0, desc.mip_levels).and_then(|whole| { + texture.view = whole; + let levels = if desc.mip_levels > 1 { + 0..desc.mip_levels + } else { + 0..0 + }; + levels.map(|level| view(level, 1)).collect() + }); + match views { + Ok(mip_views) => { + texture.mip_views = mip_views; + Ok(texture) + } Err(error) => { texture.destroy(device); - Err(format!("failed to create image view for {label}: {error}")) + Err(error) } } } @@ -605,10 +628,34 @@ impl Texture { } } + /// One level of the texture; a texture made with one level is its whole + /// view. + pub fn mip_handle(&self, level: u32) -> TextureHandle { + TextureHandle { + image: self.raw, + view: match self.mip_views.get(level as usize) { + Some(&view) => view, + None => { + assert!( + self.mip_views.is_empty() && level == 0, + "no mip level {level}" + ); + self.view + } + }, + } + } + /// Frees the texture and any cached framebuffers holding it. pub fn destroy(self, device: &Device) { - device.targets.forget(device, &[self.handle()]); - device.raw.destroy_image_view(self.view, None); + let handles: Vec = (0..self.mip_views.len() as u32) + .map(|level| self.mip_handle(level)) + .chain([self.handle()]) + .collect(); + device.targets.forget(device, &handles); + for view in self.mip_views.iter().chain([&self.view]) { + device.raw.destroy_image_view(*view, None); + } device.raw.destroy_image(self.raw, None); device.allocator().free(self.allocation).ok(); } @@ -705,6 +752,8 @@ pub fn try_create_sampler(device: &Device, desc: &SamplerDesc) -> Result vk::CompareOp::Less, Some(CompareFunction::LessEqual) | None => vk::CompareOp::LessOrEqual, + Some(CompareFunction::Greater) => vk::CompareOp::Greater, + Some(CompareFunction::Always) => vk::CompareOp::Always, }, ..Default::default() }; @@ -1033,6 +1082,8 @@ pub fn try_create_render_pipeline( depth_compare_op: match depth.compare { CompareFunction::Less => vk::CompareOp::Less, CompareFunction::LessEqual => vk::CompareOp::LessOrEqual, + CompareFunction::Greater => vk::CompareOp::Greater, + CompareFunction::Always => vk::CompareOp::Always, }, ..Default::default() }, @@ -1107,7 +1158,7 @@ impl TextureUse { Self::Undefined => vk::ImageLayout::Undefined, Self::CopySrc | Self::DepthCopySrc => vk::ImageLayout::TransferSrcOptimal, Self::CopyDst | Self::DepthCopyDst => vk::ImageLayout::TransferDstOptimal, - Self::ShaderRead => vk::ImageLayout::ShaderReadOnlyOptimal, + Self::ShaderRead | Self::ComputeRead => vk::ImageLayout::ShaderReadOnlyOptimal, Self::ColorAttachment => vk::ImageLayout::ColorAttachmentOptimal, Self::Present => vk::ImageLayout::PresentSrcKHR, Self::DepthAttachment => vk::ImageLayout::DepthStencilAttachmentOptimal, @@ -1151,6 +1202,10 @@ impl TextureUse { vk::AccessFlags::ShaderRead, vk::PipelineStageFlags::VertexShader | vk::PipelineStageFlags::FragmentShader, ), + Self::ComputeRead => ( + vk::AccessFlags::ShaderRead, + vk::PipelineStageFlags::ComputeShader, + ), Self::DepthAttachment => ( vk::AccessFlags::DepthStencilAttachmentRead | vk::AccessFlags::DepthStencilAttachmentWrite, @@ -1184,6 +1239,18 @@ impl BufferUse { vk::AccessFlags::ShaderRead | vk::AccessFlags::ShaderWrite, vk::PipelineStageFlags::ComputeShader, ), + Self::Storage => ( + vk::AccessFlags::ShaderRead | vk::AccessFlags::ShaderWrite, + vk::PipelineStageFlags::ComputeShader, + ), + Self::FragmentWrite => ( + vk::AccessFlags::ShaderWrite, + vk::PipelineStageFlags::FragmentShader, + ), + Self::ShaderRead => ( + vk::AccessFlags::ShaderRead, + vk::PipelineStageFlags::VertexShader | vk::PipelineStageFlags::FragmentShader, + ), Self::DrawInput => ( vk::AccessFlags::IndirectCommandRead | vk::AccessFlags::VertexAttributeRead @@ -1249,8 +1316,22 @@ impl CommandEncoder { /// Makes every buffer write of `from` visible to `to`. pub fn transition_buffers(&self, from: BufferUse, to: BufferUse) { - let (src_access_mask, src_stage) = from.masks(); - let (dst_access_mask, dst_stage) = to.masks(); + self.transition_buffers_many(&[(from, to)]); + } + + /// Several buffer transitions as one barrier: the whole of each use's + /// stages and accesses waits on the whole of the others', once. + pub fn transition_buffers_many(&self, transitions: &[(BufferUse, BufferUse)]) { + let (mut src_access_mask, mut src_stage) = Default::default(); + let (mut dst_access_mask, mut dst_stage) = Default::default(); + for &(from, to) in transitions { + let (access, stage) = from.masks(); + src_access_mask |= access; + src_stage |= stage; + let (access, stage) = to.masks(); + dst_access_mask |= access; + dst_stage |= stage; + } self.cmd.pipeline_barrier( src_stage, dst_stage, @@ -1265,6 +1346,12 @@ impl CommandEncoder { ); } + /// Fills `size` bytes of `buffer` from `offset` with `value`, as a + /// transfer ([`BufferUse::CopyDst`]). + pub fn fill_buffer(&self, buffer: BufferHandle, offset: u64, size: u64, value: u32) { + self.cmd.fill_buffer(buffer.0, offset, size, value); + } + pub fn copy_buffer(&self, src: BufferHandle, dst: BufferHandle, copies: &[BufferCopy]) { let regions: Vec<_> = copies .iter() @@ -1443,8 +1530,34 @@ impl CommandEncoder { ) } - /// Dispatches `groups` workgroups of `pipeline` with `group` bound at 0. - pub fn dispatch(&self, pipeline: &ComputePipeline, group: BindGroup, groups: [u32; 3]) { + /// Dispatches `groups` workgroups of `pipeline` with `group` bound at 0 + /// and `push` as its push constants. + pub fn dispatch( + &self, + pipeline: &ComputePipeline, + group: BindGroup, + push: &[u8], + groups: [u32; 3], + ) { + self.bind_compute(pipeline, group, push); + self.cmd.dispatch(groups[0], groups[1], groups[2]); + } + + /// As [`Self::dispatch`], the workgroup counts read from `buffer` at + /// `offset` ([`BufferUse::DrawInput`]). + pub fn dispatch_indirect( + &self, + pipeline: &ComputePipeline, + group: BindGroup, + push: &[u8], + buffer: BufferHandle, + offset: u64, + ) { + self.bind_compute(pipeline, group, push); + self.cmd.dispatch_indirect(buffer.0, offset); + } + + fn bind_compute(&self, pipeline: &ComputePipeline, group: BindGroup, push: &[u8]) { self.cmd .bind_pipeline(vk::PipelineBindPoint::Compute, pipeline.raw); self.cmd.bind_descriptor_sets( @@ -1454,7 +1567,10 @@ impl CommandEncoder { &[group.raw], &[], ); - self.cmd.dispatch(groups[0], groups[1], groups[2]); + if !push.is_empty() { + self.cmd + .push_constants(pipeline.layout.0, vk::ShaderStageFlags::Compute, 0, push); + } } } @@ -1983,6 +2099,125 @@ impl Frames { } } +/// GPU timestamps between a frame's passes: one set of queries per frame in +/// flight, read back once that frame's fence has signalled. +pub struct GpuTimer { + pool: vk::QueryPool, + /// Nanoseconds per tick. + period: f32, + /// The bits a stamp holds; the counter wraps past them. + valid_mask: u64, + per_frame: u32, + /// Each frame's stamps, named by what finished before them, as it + /// records them. + written: [Vec<&'static str>; MAX_FRAMES_IN_FLIGHT], + /// The names of each frame's last submitted stamps, which its queries + /// hold; a frame recorded but never submitted leaves them. + submitted: [Vec<&'static str>; MAX_FRAMES_IN_FLIGHT], +} + +impl GpuTimer { + /// None where the graphics queue has no timestamps. + pub fn new(ctx: &Context, per_frame: u32) -> Option { + let families = ctx.physical_device.get_queue_family_properties(); + let valid_bits = families + .get(ctx.graphics_family as usize) + .map_or(0, |family| family.timestamp_valid_bits); + if valid_bits == 0 { + return None; + } + let pool = ctx + .device + .raw + .create_query_pool( + &vk::QueryPoolCreateInfo { + query_type: vk::QueryType::Timestamp, + query_count: per_frame * MAX_FRAMES_IN_FLIGHT as u32, + ..Default::default() + }, + None, + ) + .ok()?; + Some(Self { + pool, + period: ctx.physical_device.get_properties().limits.timestamp_period, + valid_mask: u64::MAX >> (64 - valid_bits.min(64)), + per_frame, + written: Default::default(), + submitted: Default::default(), + }) + } + + fn base(&self, frame: usize) -> u32 { + frame as u32 * self.per_frame + } + + /// Resets the frame's queries and stamps its start. + pub fn begin_frame(&mut self, encoder: &CommandEncoder, frame: usize) { + encoder + .cmd + .reset_query_pool(self.pool, self.base(frame), self.per_frame); + self.written[frame].clear(); + self.stamp(encoder, frame, "begin"); + } + + /// Stamps the end of `what`: the moment everything recorded so far is + /// done. + pub fn stamp(&mut self, encoder: &CommandEncoder, frame: usize, what: &'static str) { + let index = self.written[frame].len() as u32; + if index >= self.per_frame { + return; + } + encoder.cmd.write_timestamp( + vk::PipelineStageFlags::BottomOfPipe, + self.pool, + self.base(frame) + index, + ); + self.written[frame].push(what); + } + + /// Marks the frame's recorded stamps as submitted. + pub fn submitted(&mut self, frame: usize) { + self.submitted[frame] = std::mem::take(&mut self.written[frame]); + } + + /// The frame's stamps as milliseconds since the one before each, once + /// its fence signalled; empty while the results aren't in. + pub fn read(&self, device: &Device, frame: usize) -> Vec<(&'static str, f32)> { + let names = &self.submitted[frame]; + let mut ticks = vec![0u64; names.len()]; + if names.len() < 2 + || device + .raw + .get_query_pool_results( + self.pool, + self.base(frame), + names.len() as u32, + bytemuck::cast_slice_mut(&mut ticks), + size_of::() as u64, + vk::QueryResultFlags::Type64, + ) + .is_err() + { + return Vec::new(); + } + ticks + .windows(2) + .zip(names.iter().skip(1)) + .map(|(pair, &name)| { + ( + name, + (pair[1].wrapping_sub(pair[0]) & self.valid_mask) as f32 * self.period / 1e6, + ) + }) + .collect() + } + + pub fn destroy(&mut self, device: &Device) { + device.raw.destroy_query_pool(self.pool, None); + } +} + /// The queues a frame is submitted and presented on. #[derive(Clone, Copy)] struct Queues { diff --git a/pomme-client/src/renderer/rhi/vulkan/swapchain.rs b/pomme-client/src/renderer/rhi/vulkan/swapchain.rs index d17aefd01..59388f99d 100644 --- a/pomme-client/src/renderer/rhi/vulkan/swapchain.rs +++ b/pomme-client/src/renderer/rhi/vulkan/swapchain.rs @@ -81,15 +81,9 @@ impl Swapchain { let present_mode = if vsync { vk::PresentModeKHR::Fifo } else { - // MoltenVK's Mailbox stays synced to the display refresh, so on - // Apple only Immediate (displaySyncEnabled = NO) truly uncaps; - // elsewhere Mailbox is preferred (uncapped and tear-free). - let prefer = if cfg!(target_vendor = "apple") { - [vk::PresentModeKHR::Immediate, vk::PresentModeKHR::Mailbox] - } else { - [vk::PresentModeKHR::Mailbox, vk::PresentModeKHR::Immediate] - }; - prefer + // Vanilla `GpuSurface.PresentMode`: vsync off takes Immediate, + // then Mailbox, then Fifo. + [vk::PresentModeKHR::Immediate, vk::PresentModeKHR::Mailbox] .into_iter() .find(|m| present_modes.contains(m)) .unwrap_or(vk::PresentModeKHR::Fifo) @@ -228,6 +222,24 @@ impl Swapchain { ) } + /// As [`Self::pass`], but the depth buffer loads as `depth` says and is + /// kept after the pass, for a frame drawn in several passes. + pub fn pass_keeping_depth( + &self, + index: u32, + color: rhi::LoadOp<[f32; 4]>, + depth: rhi::LoadOp, + ) -> super::RenderPassDesc { + super::RenderPassDesc { + depth: Some(super::DepthAttachment { + texture: self.depth.handle(), + load: depth, + store: rhi::StoreOp::Store, + }), + ..self.pass(index, color) + } + } + pub fn is_fifo(&self) -> bool { self.present_mode == vk::PresentModeKHR::Fifo } diff --git a/pomme-client/src/renderer/shaders/cull.comp b/pomme-client/src/renderer/shaders/cull.comp index 52a01694d..dc79f4eb5 100644 --- a/pomme-client/src/renderer/shaders/cull.comp +++ b/pomme-client/src/renderer/shaders/cull.comp @@ -100,12 +100,15 @@ layout(set = 0, binding = 8) readonly buffer VisibilityBuf { const uint SOLID = 0u; const uint CUTOUT = 1u; const uint WATER = 2u; +const uint MAX_ARENAS = 256u; // Emit one indexed draw covering [first..first+count) for this section into // its arena's draws of `layer`. The per-instance attributes (origin + fade) // come from meta entry `idx`, routed via first_instance. void emit(ChunkMeta m, uint base, uint count, uint first, uint idx, uint layer) { if (count == 0u) return; + // The arena's index total, after every arena's draw count. + uint indices = MAX_ARENAS + m.arena; DrawCmd d; d.index_count = count; d.instance_count = 1u; @@ -114,10 +117,13 @@ void emit(ChunkMeta m, uint base, uint count, uint first, uint idx, uint layer) d.first_instance = idx; if (layer == SOLID) { draws[base + atomicAdd(draw_counts[m.arena], 1u)] = d; + atomicAdd(draw_counts[indices], count); } else if (layer == CUTOUT) { cutout_draws[base + atomicAdd(cutout_draw_counts[m.arena], 1u)] = d; + atomicAdd(cutout_draw_counts[indices], count); } else { water_draws[base + atomicAdd(water_draw_counts[m.arena], 1u)] = d; + atomicAdd(water_draw_counts[indices], count); } } diff --git a/pomme-client/src/renderer/shaders/distant.frag b/pomme-client/src/renderer/shaders/distant.frag new file mode 100644 index 000000000..388fd780d --- /dev/null +++ b/pomme-client/src/renderer/shaders/distant.frag @@ -0,0 +1,64 @@ +#version 450 + +// The quad's face texture repeated per block from the model atlas, cut out, +// tinted and lit; nothing nearer than the far side of the drawn chunks' +// boxes (distant_bound.vert). + +#include "distant_models.glsl" + +// Every model's face textures (models.rs). +layout(set = 1, binding = 3) uniform texture2D model_atlas; +layout(set = 1, binding = 4) uniform sampler model_sampler; +layout(set = 3, binding = 1) uniform texture2D depth_bound; + +layout(location = 0) flat in uvec3 v_data; +layout(location = 1) in vec2 v_uv; + +layout(location = 0) out vec4 out_color; + +void main() { + uint flags = v_data.x; + uint face = flags >> 4u & 7u; + uint model_id = flags >> 16u; + vec2 size = vec2(flags >> 8u & 0xFu, flags >> 12u & 0xFu); + // The block within the quad, and the texel within its face. + vec2 tile; + vec2 texel = modf(v_uv, tile); + vec2 face_origin = vec2(model_id % ATLAS_COLUMNS, model_id / ATLAS_COLUMNS) * TILE_SIZE + + vec2(face >> 1u, face & 1u) * FACE_SIZE; + vec2 pos = (face_origin + texel * FACE_SIZE) / ATLAS_SIZE; + // Mipped by how fast the unwrapped coordinate moves, so the wrap from + // block to block doesn't pick the smallest mip; taken before any + // fragment leaves. + vec2 unwrapped = v_uv * (FACE_SIZE / ATLAS_SIZE); + vec2 grad_x = dFdx(unwrapped); + vec2 grad_y = dFdy(unwrapped); + if (any(notEqual(clamp(tile, vec2(0.0), size), tile))) { + discard; + } + if (gl_FragCoord.z < texelFetch(sampler2D(depth_bound, model_sampler), ivec2(gl_FragCoord.xy), 0).r) { + discard; + } + vec4 color = textureGrad(sampler2D(model_atlas, model_sampler), pos, grad_x, grad_y); + // Cut out by the unmipped alpha, which mips would smear. + vec4 exact = textureLod(sampler2D(model_atlas, model_sampler), pos, 0.0); + if ((flags & 2u) != 0u) { + if (exact.a == 0.0) { + discard; + } + } else { + color.a = 1.0; + if ((flags & 1u) != 0u && exact.a <= 0.1) { + discard; + } + } + // Tint state 2 everywhere, 1 only on grey texels, as a grass block's + // side. + uint tint = flags >> 2u & 3u; + bool grey = abs(exact.r - exact.g) < 0.02 && abs(exact.g - exact.b) < 0.02; + if (tint == 2u || (tint == 1u && grey)) { + color.rgb *= unpackUnorm4x8(v_data.z).rgb; + } + color.rgb *= unpackUnorm4x8(v_data.y).rgb; + out_color = color; +} diff --git a/pomme-client/src/renderer/shaders/distant.vert b/pomme-client/src/renderer/shaders/distant.vert new file mode 100644 index 000000000..6b0ac2abc --- /dev/null +++ b/pomme-client/src/renderer/shaders/distant.vert @@ -0,0 +1,101 @@ +#version 450 + +// Distant terrain (renderer/distant): each quad is eight bytes in the bound +// arena, read by `gl_VertexIndex >> 2`; its corners come from its model's +// face data, lit through the lightmap. Fog comes later, in distant_blit.frag. + +#include "lightmap.glsl" +#include "distant_models.glsl" +#define DISTANT_SET 0 +#define SCENE_BINDING 0 +#include "distant_section.glsl" + +layout(set = 1, binding = 0) readonly buffer Quads { + uvec2 quads[]; +}; +layout(set = 1, binding = 1) readonly buffer Models { + BlockModel models[]; +}; +// Biome tints by kind (distant_models.glsl TINT_BIOMES per kind). +layout(set = 1, binding = 2) readonly buffer Tints { + uint tints[]; +}; +// Per draw (`gl_InstanceIndex`): the section's packed key. +layout(set = 3, binding = 0) readonly buffer Positions { + uvec2 positions[]; +}; + +// Quad flags (cutout bit 0, translucent 1, tint state 2-3, face 4-6, +// width-1 8-11, height-1 12-15, model 16-31), the lightmap colour, the tint +// colour. +layout(location = 0) flat out uvec3 v_data; +layout(location = 1) out vec2 v_uv; + +uint field(uvec2 quad, uint bits, uint shift) { + uint word = shift < 32u ? quad.x : quad.y; + return (word >> (shift & 31u)) & ((1u << bits) - 1u); +} + +// A plane's two extents and its depth put on the axes the face's normal +// leaves free. +vec3 swizzle(uint axis, vec3 data) { + return axis == 0u ? data.xzy : (axis == 1u ? data : data.zxy); +} + +void main() { + // The section's corner relative to the camera's section, and its level's + // scale, blocks per voxel. + uvec2 pos = positions[gl_InstanceIndex]; + uint lod = lod_level(pos); + ivec3 base_section = (lod_position(pos) << lod) - camera_section; + vec4 origin_scale = vec4(vec3(base_section << 5), float(1u << lod)); + uvec2 quad = quads[gl_VertexIndex >> 2]; + uint corner = gl_VertexIndex & 3u; + uint face = field(quad, 3u, 0u); + uint axis = face >> 1u; + uint model_id = field(quad, 6u, 26u) | field(quad, 14u, 32u) << 6u; + BlockModel model = models[model_id]; + uint face_data = model.face_data[face]; + vec2 size = vec2(field(quad, 4u, 3u), field(quad, 4u, 7u)) + 1.0; + + // The drawn texels' span, grown a hair so quads meet. + vec4 bounds = vec4(face_data & 0xFu, face_data >> 4u & 0xFu, face_data >> 8u & 0xFu, face_data >> 12u & 0xFu) / 16.0 + + vec4(0.0, 1.0 / 16.0, 0.0, 1.0 / 16.0); + bounds.xz -= 0.00005; + bounds.yw -= bounds.xz; + uint inset = face_data >> 16u & 63u; + inset += uint(inset == 63u); + float depth = float(inset) / 64.0; + + vec3 start = vec3(field(quad, 5u, 21u), field(quad, 5u, 16u), field(quad, 5u, 11u)); + start += swizzle(axis, vec3(bounds.xz, mix(depth, 1.0 - depth, float(face & 1u)))); + vec2 extent = bounds.yw + size - 1.0; + vec2 mask = vec2(corner >> 1u & 1u, corner & 1u); + vec3 point = origin_scale.xyz + (start + swizzle(axis, vec3(extent * mask, 0.0))) * origin_scale.w; + gl_Position = distant_view_proj * vec4(point, 1.0); + v_uv = bounds.xz + extent * mask; + + // A face short of its side cuts out once a quad spans more than one + // block. + uint flags = face_data >> 22u & 1u; + flags |= uint(any(greaterThan(size, vec2(1.0)))) & (face_data >> 23u & 1u); + flags |= uint((model.flags & MODEL_TRANSLUCENT) != 0u) << 1u; + flags |= (face_data >> 24u & 3u) << 2u; + flags |= face << 4u; + flags |= (uint(size.x) - 1u) << 8u | (uint(size.y) - 1u) << 12u; + flags |= model_id << 16u; + + // The quad's light (block high, sky low) through the lightmap, shaded by + // the dimension's cardinal lighting: per face for models that take + // directional shade, as an up face otherwise; the model's tint, constant + // or by the quad's biome. + uint light = field(quad, 8u, 55u); + vec3 lit = sample_lightmap(vec2(float(light >> 4u), float(light & 0xFu)) * 16.0).rgb; + uint shade_face = (model.flags & MODEL_SHADED) != 0u ? face : 1u; + lit *= face_shade[shade_face >> 2u][shade_face & 3u]; + uint tint = model.tint; + if ((model.flags & MODEL_BIOME_TINT) != 0u) { + tint = tints[tint * TINT_BIOMES + field(quad, 9u, 46u)]; + } + v_data = uvec3(flags, packUnorm4x8(vec4(lit, 1.0)), tint); +} diff --git a/pomme-client/src/renderer/shaders/distant_blit.frag b/pomme-client/src/renderer/shaders/distant_blit.frag new file mode 100644 index 000000000..96d8d4c60 --- /dev/null +++ b/pomme-client/src/renderer/shaders/distant_blit.frag @@ -0,0 +1,47 @@ +#version 450 + +// Distant terrain's image laid into the frame, its depth carried over from +// the distant projection to the camera's so what's drawn after still sorts +// against it, fogged like the chunks and faded out over the last tenth of +// its distance. + +#include "camera_ubo.glsl" +#include "fog.glsl" + +layout(push_constant) uniform BlitPc { + // The inverse of the view projection distant terrain drew with. + mat4 inverse_distant_view_proj; + // The fade: `alpha *= 1 - saturate(distance * fade.y + fade.x)`. + vec4 fade; +}; + +layout(set = 1, binding = 0) uniform texture2D distant_color; +layout(set = 1, binding = 1) uniform texture2D distant_depth; +layout(set = 1, binding = 2) uniform sampler distant_sampler; + +layout(location = 0) in vec2 v_uv; + +layout(location = 0) out vec4 out_color; + +void main() { + ivec2 pixel = ivec2(gl_FragCoord.xy); + float depth = texelFetch(sampler2D(distant_depth, distant_sampler), pixel, 0).r; + // 0 where the chunks drew (distant_setup.frag), 1 where nothing did. + if (depth == 0.0 || depth == 1.0) { + discard; + } + vec4 point = inverse_distant_view_proj * vec4(v_uv * 2.0 - 1.0, depth, 1.0); + vec3 rel = point.xyz / point.w; + vec4 clip = view_proj * vec4(rel, 1.0); + // Kept just inside the far plane, where the chunks' depth clears to. + gl_FragDepth = min(clip.z / clip.w, 1.0 - 2.0 / 16777215.0); + + vec4 color = texelFetch(sampler2D(distant_color, distant_sampler), pixel, 0); + if (color.a == 0.0) { + discard; + } + float fog = total_fog_value(rel, fog_env, camera_pos.w, fog_color.w); + color.rgb = apply_fog(color.rgb, fog, fog_color.rgb); + color.a *= 1.0 - clamp(length(rel.xz) * fade.y + fade.x, 0.0, 1.0); + out_color = color; +} diff --git a/pomme-client/src/renderer/shaders/distant_bound.frag b/pomme-client/src/renderer/shaders/distant_bound.frag new file mode 100644 index 000000000..a7756e622 --- /dev/null +++ b/pomme-client/src/renderer/shaders/distant_bound.frag @@ -0,0 +1,5 @@ +#version 450 + +// Depth alone. + +void main() {} diff --git a/pomme-client/src/renderer/shaders/distant_bound.vert b/pomme-client/src/renderer/shaders/distant_bound.vert new file mode 100644 index 000000000..6b7ea5f8f --- /dev/null +++ b/pomme-client/src/renderer/shaders/distant_bound.vert @@ -0,0 +1,44 @@ +#version 450 + +// A box per run of drawn chunk sections, drawn depth-only so the far side of +// each box bounds what distant terrain may draw within loaded chunks +// (distant.frag discards nearer fragments). + +#include "camera_ubo.glsl" +#include "distant_section.glsl" + +layout(push_constant) uniform BoundPc { + // The camera's view projection with the distant near and far planes. + mat4 distant_view_proj; + // The render distance in blocks; boxes past it leave distant terrain be. + float render_distance; +}; + +// Each box's section coordinates and its height in sections (gpu.rs +// `BoundStore`). +layout(set = 1, binding = 0) readonly buffer Sections { + ivec4 sections[]; +}; + +// Whether the box from `lo` to `hi` (camera-relative, grown a block) has a +// point within the render distance of the camera. +bool should_render(ivec3 lo, ivec3 hi) { + vec3 nearest = mix(mix(vec3(0.0), vec3(lo), greaterThan(lo, ivec3(0))), vec3(hi), lessThan(hi, ivec3(0))) + - camera_pos.xyz; + return dot(nearest.xz, nearest.xz) < render_distance * render_distance + && abs(nearest.y) < render_distance; +} + +void main() { + ivec4 box = sections[gl_InstanceIndex]; + ivec3 origin = box.xyz * 16 - camera_block.xyz; + ivec3 size = ivec3(16, box.w * 16, 16); + if (!should_render(origin - 1, origin + size + 1)) { + gl_Position = vec4(-100.0, -100.0, -100.0, 0.0); + return; + } + vec3 point = vec3(origin) + cube_corner(gl_VertexIndex) * vec3(size); + gl_Position = distant_view_proj * vec4(point - camera_pos.xyz, 1.0); + // Brought a hair closer, so a face on the box's edge still counts as inside. + gl_Position.z -= 0.0005; +} diff --git a/pomme-client/src/renderer/shaders/distant_cleaner_results.comp b/pomme-client/src/renderer/shaders/distant_cleaner_results.comp new file mode 100644 index 000000000..f731895d2 --- /dev/null +++ b/pomme-client/src/renderer/shaders/distant_cleaner_results.comp @@ -0,0 +1,44 @@ +#version 450 + +// The sorted ids become their nodes' keys for the CPU (all ones for a freed +// node) beside the ids, which the CPU checks still hold those nodes; each +// node's last render frame pushed 60 frames ahead so it isn't picked again +// at once. The sentinel the output started filled with keeps its frame (a +// departure: the reference pushes it too, and as frames pass its stale frame +// bars every node drawn since from the sort). + +#include "distant_consts.glsl" + +#define OUTPUT_SIZE CLEANER_OUTPUT + +layout(local_size_x = OUTPUT_SIZE) in; + +layout(set = 0, binding = 0, std430) readonly restrict buffer MinVisibility { + uint min_vis_ids[OUTPUT_SIZE]; +}; + +layout(set = 0, binding = 1, std430) readonly restrict buffer NodeData { + uvec4 nodes[]; +}; + +layout(set = 0, binding = 2, std430) writeonly restrict buffer Output { + uvec4 output_keys[OUTPUT_SIZE]; +}; + +layout(set = 0, binding = 3, std430) writeonly restrict buffer LastRenderFrame { + uint last_render_frame[]; +}; + +layout(push_constant) uniform ResultsPc { + uint frame_id; +}; + +void main() { + uint id = min_vis_ids[gl_LocalInvocationID.x]; + uvec4 node = nodes[id]; + uvec2 key = node == uvec4(0xFFFFFFFFu) ? uvec2(0xFFFFFFFFu) : node.xy; + output_keys[gl_LocalInvocationID.x] = uvec4(key, id, 0u); + if (id != CLEANER_SENTINEL) { + last_render_frame[id] = frame_id + 60u; + } +} diff --git a/pomme-client/src/renderer/shaders/distant_cleaner_sort.comp b/pomme-client/src/renderer/shaders/distant_cleaner_sort.comp new file mode 100644 index 000000000..9a606c6d2 --- /dev/null +++ b/pomme-client/src/renderer/shaders/distant_cleaner_sort.comp @@ -0,0 +1,130 @@ +#version 450 + +// The 256 nodes drawn longest ago, for the CPU to drop the meshes of. A +// fuzzy sort: each workgroup starts from a copy of the output, works its +// nodes into it by exchanging ids along it, then works the newcomers into +// the output the same way. Only nodes with a mesh, below the top level and +// without a request in flight take part. + +#include "distant_section.glsl" + +#define WORK_SIZE CLEANER_SORT_THREADS +#define ELEMS_PER_THREAD CLEANER_SORT_ELEMS +#define OUTPUT_SIZE CLEANER_OUTPUT +#define OPS_PER_THREAD (OUTPUT_SIZE / WORK_SIZE) +// An id copied in from the output, not to be worked into it again. +#define EXTERNAL (1u << 31u) +#define NEVER 0xFFFFFFFFu + +layout(local_size_x = WORK_SIZE) in; + +layout(set = 0, binding = 0, std430) readonly restrict buffer LastRenderFrame { + uint last_render_frame[]; +}; + +layout(set = 0, binding = 1, std430) volatile restrict buffer MinVisibility { + uint min_vis_ids[OUTPUT_SIZE]; +}; + +layout(set = 0, binding = 2, std430) readonly restrict buffer NodeData { + uvec4 nodes[]; +}; + +shared uint initial_sort[OUTPUT_SIZE]; + +// Puts `id` at `at` of `list` when the id there was drawn later, giving back +// the one displaced to carry on with; `id` itself when not; NEVER when `id` +// is already there. Once for the workgroup's list and once for the output. +#define DEFINE_EXCHANGE(name, list) \ +uint name(uint at, uint id) { \ + uint value = last_render_frame[id]; \ + while (true) { \ + uint existing = list[at]; \ + if (existing == id) { \ + return NEVER; \ + } \ + if (last_render_frame[existing & ~EXTERNAL] <= value) { \ + return id; \ + } \ + if (atomicCompSwap(list[at], existing, id) == existing) { \ + return existing; \ + } \ + } \ +} +DEFINE_EXCHANGE(exchange_local, initial_sort) +DEFINE_EXCHANGE(exchange_global, min_vis_ids) + +// The list's ids carry the EXTERNAL flag, masked off to index the frames. +void bubble_initial(uint vis, uint id) { + uint start = 0u; + if (last_render_frame[initial_sort[(OUTPUT_SIZE - 1u) >> 1u] & ~EXTERNAL] <= vis) { + start = (OUTPUT_SIZE - 1u) >> 1u; + } + for (uint i = start; i < OUTPUT_SIZE; i++) { + id = exchange_local(i, id); + } +} + +void bubble_global(uint start, uint id) { + for (uint i = start; i < OUTPUT_SIZE; i++) { + id = exchange_global(i, id); + if (id == NEVER) { + break; + } + } +} + +bool should_sort(uint id) { + uvec4 node = nodes[id]; + if (node == uvec4(NEVER)) { + return false; + } + uint mesh = node.z & 0xFFFFFFu; + if (mesh == EMPTY_ID || mesh == NULL_ID) { + return false; + } + if (lod_level(node.xy) == LEVELS - 1u) { + return false; + } + return (node.z & (1u << 24u)) == 0u; +} + +void main() { + for (uint i = 0u; i < OPS_PER_THREAD; i++) { + uint slot = gl_LocalInvocationID.x * OPS_PER_THREAD + i; + initial_sort[slot] = min_vis_ids[slot] | EXTERNAL; + } + barrier(); + for (uint i = 0u; i < ELEMS_PER_THREAD; i++) { + uint id = gl_GlobalInvocationID.x * ELEMS_PER_THREAD + i; + uint vis = last_render_frame[id]; + if (vis == NEVER) { + continue; + } + if (last_render_frame[min_vis_ids[OUTPUT_SIZE - 1u]] <= vis) { + continue; + } + if (!should_sort(id)) { + continue; + } + bubble_initial(vis, id); + } + barrier(); + for (uint i = 0u; i < OPS_PER_THREAD; i++) { + barrier(); + uint slot = gl_LocalInvocationID.x * OPS_PER_THREAD + i; + uint sid = initial_sort[slot]; + if ((sid & EXTERNAL) != 0u) { + continue; + } + uint vis = last_render_frame[sid]; + if (last_render_frame[min_vis_ids[OUTPUT_SIZE - 1u]] <= vis) { + continue; + } + uint start = slot; + if (last_render_frame[min_vis_ids[(slot + OUTPUT_SIZE) >> 1u]] <= vis) { + start = (slot + OUTPUT_SIZE) >> 1u; + } + bubble_global(start, sid); + } +} diff --git a/pomme-client/src/renderer/shaders/distant_cmdgen.comp b/pomme-client/src/renderer/shaders/distant_cmdgen.comp new file mode 100644 index 000000000..43bd49152 --- /dev/null +++ b/pomme-client/src/renderer/shaders/distant_cmdgen.comp @@ -0,0 +1,100 @@ +#version 450 + +// A draw per face group of every listed section the occlusion pass saw this +// frame, face groups turned away from the camera left out; the same draws +// again in the temporal region for sections it didn't see last frame, since +// this frame's opaque pass drew last frame's list; translucent sections +// counted into distance buckets for distant_translucents.comp. + +#define DISTANT_SET 0 +#define SCENE_BINDING 0 +#define DRAW_BUFFER_BINDING 1 +#define DRAW_COUNTS_BINDING 2 +#define SECTION_METADATA_BINDING 3 +#define VISIBILITY_BINDING 4 +#define RENDER_LIST_BINDING 5 +#define POSITION_BINDING 6 +#define TRANSLUCENT_DATA_BINDING 7 +#include "distant_section.glsl" + +layout(local_size_x = LIST_GROUP) in; + +void main() { + uint draw_id = gl_GlobalInvocationID.x; + if (draw_id >= listed_count()) { + return; + } + uint section_id = render_list[draw_id]; + SectionMeta meta = section_data[section_id]; + uint detail = section_detail(meta); + ivec3 ipos = section_position(meta); + + uint seen = visibility_data[section_id]; + if (occlusion_skipped != 0u) { + // No occlusion pass: every listed section counts as seen. + seen = seen_now(seen); + visibility_data[section_id] = seen; + } + bool should_render = (seen & ~SEEN_BEFORE_BIT) == frame_id; + bool render_temporally = (seen & SEEN_BEFORE_BIT) == 0u; + if (!should_render) { + return; + } + + uint ptr = section_quad_start(meta); + ivec3 relative = ipos - (camera_section >> detail); + position_buffer[draw_id] = meta.a.xy; + uvec4 counts = meta.b; + + // Down, up, north, south, west, east: only the groups facing the camera. + uint mask = 0u; + mask |= uint((counts.y & 0xFFFFu) != 0u && relative.y > -1) << 0u; + mask |= uint((counts.y >> 16u) != 0u && relative.y < 1) << 1u; + mask |= uint((counts.z & 0xFFFFu) != 0u && relative.z > -1) << 2u; + mask |= uint((counts.z >> 16u) != 0u && relative.z < 1) << 3u; + mask |= uint((counts.w & 0xFFFFu) != 0u && relative.x > -1) << 4u; + mask |= uint((counts.w >> 16u) != 0u && relative.x < 1) << 5u; + mask |= uint((counts.x >> 16u) != 0u) << 6u; + + uint cmd_count = bitCount(mask); + uint cmd_ptr = atomicAdd(opaque_draw_count, cmd_count); + uint temporal_ptr = 0u; + if (render_temporally) { + temporal_ptr = atomicAdd(temporal_draw_count, cmd_count) + TEMPORAL_OFFSET; + } + + // Translucent quads: bucketed by distance, nearest in the last bucket; + // past a full list neither listed nor counted, so the buckets' sums stay + // within it. + uint count = counts.x & 0xFFFFu; + uint slot = count != 0u ? atomicAdd(translucent_draw_count, 1u) : TRANSLUCENT_DRAWS; + if (slot < TRANSLUCENT_DRAWS) { + translucent_data[slot + TRANSLUCENT_BUCKETS] = draw_id; + uvec3 abs_rel = abs(relative); + uint distance = (abs_rel.x + abs_rel.y + abs_rel.z) << detail; + distance = (TRANSLUCENT_BUCKETS - 1u) - min(distance, TRANSLUCENT_BUCKETS - 1u); + atomicAdd(translucent_data[distance], 1u); + } + ptr += count; + + // Double-sided quads, then the six face groups. + count = counts.x >> 16u; + if ((mask & (1u << 6u)) != 0u) { + write_draw(cmd_ptr++, OPAQUE_END, draw_id, ptr, count); + if (render_temporally) { + write_draw(temporal_ptr++, TEMPORAL_END, draw_id, ptr, count); + } + } + ptr += count; + for (uint face = 0u; face < 6u; face++) { + uint word = counts[1u + (face >> 1u)]; + count = (face & 1u) == 0u ? word & 0xFFFFu : word >> 16u; + if ((mask & (1u << face)) != 0u) { + write_draw(cmd_ptr++, OPAQUE_END, draw_id, ptr, count); + if (render_temporally) { + write_draw(temporal_ptr++, TEMPORAL_END, draw_id, ptr, count); + } + } + ptr += count; + } +} diff --git a/pomme-client/src/renderer/shaders/distant_cull.frag b/pomme-client/src/renderer/shaders/distant_cull.frag new file mode 100644 index 000000000..d99bcbbf6 --- /dev/null +++ b/pomme-client/src/renderer/shaders/distant_cull.frag @@ -0,0 +1,16 @@ +#version 450 + +// A fragment that passes the depth test marks its section seen this frame. + +#define DISTANT_SET 0 +#define VISIBILITY_BINDING 2 +#include "distant_section.glsl" + +layout(early_fragment_tests) in; + +layout(location = 0) flat in uint v_id; +layout(location = 1) flat in uint v_value; + +void main() { + visibility_data[v_id] = v_value; +} diff --git a/pomme-client/src/renderer/shaders/distant_cull.vert b/pomme-client/src/renderer/shaders/distant_cull.vert new file mode 100644 index 000000000..b91d3df5a --- /dev/null +++ b/pomme-client/src/renderer/shaders/distant_cull.vert @@ -0,0 +1,32 @@ +#version 450 + +// Each listed section's quad bounds as a box, grown by a block and brought a +// hair closer, depth-tested against the distant image's depth; the fragment +// stage records the sections that show. + +#define DISTANT_SET 0 +#define SCENE_BINDING 0 +#define SECTION_METADATA_BINDING 1 +#define VISIBILITY_BINDING 2 +#define VISIBILITY_ACCESS readonly +#define RENDER_LIST_BINDING 3 +#include "distant_section.glsl" + +layout(location = 0) flat out uint v_id; +layout(location = 1) flat out uint v_value; + +void main() { + uint sid = render_list[gl_InstanceIndex]; + SectionMeta section = section_data[sid]; + uint detail = section_detail(section); + ivec3 pos = ((section_position(section) << detail) - camera_section) << 5; + const float EXPANSION = 1.0; + vec3 offset = vec3(section_aabb_offset(section)) - EXPANSION; + offset += cube_corner(gl_VertexIndex) * (vec3(section_aabb_size(section)) + 2.0 * EXPANSION); + gl_Position = distant_view_proj * vec4(vec3(pos) + offset * float(1 << detail), 1.0); + gl_Position.z -= 0.000001 * gl_Position.w; + + v_id = sid; + // Whether the pass saw it last frame, for the temporal draws. + v_value = seen_now(visibility_data[sid]); +} diff --git a/pomme-client/src/renderer/shaders/distant_hiz.frag b/pomme-client/src/renderer/shaders/distant_hiz.frag new file mode 100644 index 000000000..1dd4ff199 --- /dev/null +++ b/pomme-client/src/renderer/shaders/distant_hiz.frag @@ -0,0 +1,17 @@ +#version 450 + +// Each level of the Hi-Z holds the farthest of the four texels under it in +// the level before (level 0 reads the distant image's depth), so a box whose +// nearest point lies beyond every texel it covers is hidden. A texel at the +// far plane needs no special case: the max already carries it up. + +layout(set = 0, binding = 0) uniform texture2D source; +layout(set = 0, binding = 1) uniform sampler source_sampler; + +layout(location = 0) in vec2 v_uv; +layout(location = 0) out vec4 out_depth; + +void main() { + vec4 depths = textureGather(sampler2D(source, source_sampler), v_uv, 0); + out_depth = vec4(max(max(depths.x, depths.y), max(depths.z, depths.w))); +} diff --git a/pomme-client/src/renderer/shaders/distant_models.glsl b/pomme-client/src/renderer/shaders/distant_models.glsl new file mode 100644 index 000000000..9dfd6f8ac --- /dev/null +++ b/pomme-client/src/renderer/shaders/distant_models.glsl @@ -0,0 +1,22 @@ +// Distant terrain's models (renderer/distant/models.rs): the atlas, a tile +// per model with its six face textures two rows of three, and the record of +// each. +const uint ATLAS_COLUMNS = 64u; +const vec2 TILE_SIZE = vec2(48.0, 32.0); +const vec2 ATLAS_SIZE = vec2(ATLAS_COLUMNS, 32u) * TILE_SIZE; +const float FACE_SIZE = 16.0; + +// Per face: the drawn texels' min x, max x, min y, max y in 4 bits each, +// the inset from its side in 64ths from bit 16, cutout at 22 and +// cutout-once-merged at 23, the tint state in 24-25. +struct BlockModel { + uint face_data[6]; + uint flags; + // ABGR with MODEL_BIOME_TINT clear, else the tint kind. + uint tint; +}; + +const uint MODEL_BIOME_TINT = 2u; +const uint MODEL_TRANSLUCENT = 4u; +const uint MODEL_SHADED = 8u; +const uint TINT_BIOMES = 512u; diff --git a/pomme-client/src/renderer/shaders/distant_prefix_sum.comp b/pomme-client/src/renderer/shaders/distant_prefix_sum.comp new file mode 100644 index 000000000..e37940155 --- /dev/null +++ b/pomme-client/src/renderer/shaders/distant_prefix_sum.comp @@ -0,0 +1,60 @@ +#version 450 + +// The translucent distance buckets turned into exclusive prefix sums in +// place, one workgroup over the counts as `uvec4`s. + +#include "distant_consts.glsl" + +#define WORK_SIZE 256 +#if WORK_SIZE * 4 != TRANSLUCENT_BUCKETS +#error "a uvec4 of buckets per invocation" +#endif +layout(local_size_x = WORK_SIZE) in; + +layout(set = 0, binding = 0, std430) buffer Counts { + uvec4 io_count[]; +}; + +shared uint prefix_sum[WORK_SIZE]; + +#define STEP(i) if ((I & (1u << i)) != 0u) prefix_sum[I] += prefix_sum[(I & ~((1u << (i + 1)) - 1u)) + ((1u << i) - 1u)]; + +uint prefix_sum_exclusive(uint value) { + uint I = gl_LocalInvocationID.x; + prefix_sum[I] = value; + barrier(); + STEP(0) + barrier(); + STEP(1) + barrier(); + STEP(2) + barrier(); + STEP(3) + barrier(); + STEP(4) + barrier(); + STEP(5) + barrier(); + STEP(6) + barrier(); + STEP(7) + barrier(); + return prefix_sum[I] - value; +} + +void main() { + uint gid = gl_LocalInvocationID.x; + prefix_sum[gid] = 0u; + uvec4 count = uvec4(0u); + uint sum = 0u; + { + uvec4 dat = io_count[gid]; + count.yzw = dat.xyz; + count.z += count.y; + count.w += count.z; + sum = count.w + dat.w; + } + barrier(); + count += prefix_sum_exclusive(sum); + io_count[gid] = count; +} diff --git a/pomme-client/src/renderer/shaders/distant_prep.comp b/pomme-client/src/renderer/shaders/distant_prep.comp new file mode 100644 index 000000000..b2ed4f05a --- /dev/null +++ b/pomme-client/src/renderer/shaders/distant_prep.comp @@ -0,0 +1,26 @@ +#version 450 + +// Before the command generation: its dispatch size from the render list, the +// draw counts back to zero, and the occlusion pass's draw: a box per listed +// section, from the cube indices after the quads' in the shared index +// buffer. + +layout(local_size_x = 1) in; + +#define DISTANT_SET 0 +#define DRAW_COUNTS_BINDING 0 +#define RENDER_LIST_BINDING 1 +#include "distant_section.glsl" + +void main() { + uint listed = listed_count(); + cmdgen_dispatch = uvec3((listed + LIST_GROUP - 1u) / LIST_GROUP, 1u, 1u); + opaque_draw_count = 0u; + translucent_draw_count = 0u; + temporal_draw_count = 0u; + cull_draw.count = 36u; + cull_draw.instance_count = listed; + cull_draw.first_index = QUADS_PER_DRAW * 6u; + cull_draw.base_vertex = 0; + cull_draw.base_instance = 0u; +} diff --git a/pomme-client/src/renderer/shaders/distant_scatter.comp b/pomme-client/src/renderer/shaders/distant_scatter.comp new file mode 100644 index 000000000..c04000390 --- /dev/null +++ b/pomme-client/src/renderer/shaders/distant_scatter.comp @@ -0,0 +1,40 @@ +#version 450 + +// Section records and nodes written where the frame's scatter list says, so +// a frame's uploads land at one point of the frame. The list is 80-byte +// chunks: a `uvec4` of four destination indices (bit 31 picks the node +// buffer), then the four `uvec4`s to write there. + +#include "distant_consts.glsl" + +layout(local_size_x = LIST_GROUP) in; + +layout(set = 0, binding = 0, std430) readonly restrict buffer Input { + uvec4 in_data[]; +}; +layout(set = 0, binding = 1, std430) writeonly restrict buffer Sections { + uvec4 section_data[]; +}; +layout(set = 0, binding = 2, std430) writeonly restrict buffer Nodes { + uvec4 node_data[]; +}; + +layout(push_constant) uniform ScatterPc { + uint count; +}; + +void main() { + uint id = gl_GlobalInvocationID.x; + if (count <= id) { + return; + } + uint chunk = (id >> 2u) * 5u; + uint inner = id & 3u; + uint location = in_data[chunk][inner]; + uvec4 data = in_data[chunk + 1u + inner]; + if ((location & NODE_SCATTER_BIT) != 0u) { + node_data[location & ~NODE_SCATTER_BIT] = data; + } else { + section_data[location] = data; + } +} diff --git a/pomme-client/src/renderer/shaders/distant_section.glsl b/pomme-client/src/renderer/shaders/distant_section.glsl new file mode 100644 index 000000000..c640586ca --- /dev/null +++ b/pomme-client/src/renderer/shaders/distant_section.glsl @@ -0,0 +1,185 @@ +// Distant terrain's GPU records (renderer/distant/gpu.rs): the scene +// uniform, a section's metadata, and the buffers the command generation +// passes share, each declared by the shader that defines its binding. + +#include "distant_consts.glsl" + +// The scene uniform: the distant view projection moved by the camera's +// position within its section, that section, the frame, and whether the +// occlusion pass was skipped (cmdgen then keeps visibility itself); then +// the traversal's: the Hi-Z's base size (width << 16 | height), the screen +// area a node splits at, the frame's request queue's room, the render +// distance squared in blocks, the frustum planes, and those +// of the wider view meshes are requested within; last the dimension's shade +// per face (down, up, north, south, then west, east). +#ifdef SCENE_BINDING +layout(set = DISTANT_SET, binding = SCENE_BINDING, std140) uniform SceneUniform { + mat4 distant_view_proj; + ivec3 camera_section; + uint frame_id; + vec3 camera_sub_pos; + uint occlusion_skipped; + uint hiz_size; + float min_sss; + uint request_limit; + float render_distance; + vec4 frustum[6]; + vec4 request_frustum[6]; + vec4 face_shade[2]; +}; +#endif + +// A section's packed key (section.rs `SectionKey::pack`, high word first): +// the level in the top nibble, then y (8 bits), z and x (24 each). +uint lod_level(uvec2 pos) { + return pos.x >> 28u; +} + +ivec3 lod_position(uvec2 pos) { + int y = (int(pos.x) << 4) >> 24; + int x = (int(pos.y) << 4) >> 8; + int z = int((pos.x & ((1u << 20u) - 1u)) << 4u); + z |= int(pos.y >> 28u); + z <<= 8; + z >>= 8; + return ivec3(x, y, z); +} + +// A section's record: the key, the quads' bounds and their start; then the +// eight groups' quad counts in `u16` pairs. +struct SectionMeta { + uvec4 a; + uvec4 b; +}; + +uint section_detail(SectionMeta meta) { + return lod_level(meta.a.xy); +} + +ivec3 section_position(SectionMeta meta) { + return lod_position(meta.a.xy); +} + +uint section_quad_start(SectionMeta meta) { + return meta.a.w; +} + +ivec3 section_aabb_offset(SectionMeta meta) { + return (ivec3(meta.a.z) >> ivec3(0, 5, 10)) & 31; +} + +// At least 1x1x1. +ivec3 section_aabb_size(SectionMeta meta) { + return ((ivec3(meta.a.z) >> ivec3(15, 20, 25)) & 31) + 1; +} + +// `VkDrawIndexedIndirectCommand`. +struct DrawCommand { + uint count; + uint instance_count; + uint first_index; + int base_vertex; + uint base_instance; +}; + +// The draw list's regions' ends. +const uint OPAQUE_END = OPAQUE_DRAWS; +const uint TRANSLUCENT_END = TRANSLUCENT_OFFSET + TRANSLUCENT_DRAWS; +const uint TEMPORAL_END = TEMPORAL_OFFSET + TEMPORAL_DRAWS; + +#ifdef SCENE_BINDING +// A section's visibility word for being seen this frame: the frame, and +// whether `previous` was last frame. +uint seen_now(uint previous) { + bool before = (previous & ~SEEN_BEFORE_BIT) == frame_id - 1u; + return (frame_id & ~SEEN_BEFORE_BIT) | (before ? SEEN_BEFORE_BIT : 0u); +} +#endif + +// The shared cube indices give a box corner's bits: x, y, z. +vec3 cube_corner(uint vertex) { + return vec3(vertex & 1u, vertex >> 1u & 1u, vertex >> 2u); +} + +#ifdef SECTION_METADATA_BINDING +layout(set = DISTANT_SET, binding = SECTION_METADATA_BINDING, std430) readonly restrict buffer SectionBuffer { + SectionMeta section_data[]; +}; +#endif + +// The sections the traversal picked this frame (`RENDER_LIST_ACCESS` empty +// for the traversal, which writes it). +#ifdef RENDER_LIST_BINDING +#ifndef RENDER_LIST_ACCESS +#define RENDER_LIST_ACCESS readonly +#endif +layout(set = DISTANT_SET, binding = RENDER_LIST_BINDING, std430) RENDER_LIST_ACCESS restrict buffer RenderList { + uint section_count; + uint render_list[]; +}; + +// The traversal's count can overshoot the list as it races to fill it. +uint listed_count() { + return min(section_count, MAX_LISTED); +} +#endif + +// cmdgen's dispatch size, the three regions' draw counts, and the occlusion +// pass's draw. +#ifdef DRAW_COUNTS_BINDING +layout(set = DISTANT_SET, binding = DRAW_COUNTS_BINDING, std430) restrict buffer DrawCounts { + uvec3 cmdgen_dispatch; + layout(offset = COUNTS_OPAQUE) uint opaque_draw_count; + layout(offset = COUNTS_TRANSLUCENT) uint translucent_draw_count; + layout(offset = COUNTS_TEMPORAL) uint temporal_draw_count; + layout(offset = COUNTS_CULL_DRAW) DrawCommand cull_draw; +}; +#endif + +#ifdef DRAW_BUFFER_BINDING +layout(set = DISTANT_SET, binding = DRAW_BUFFER_BINDING, std430) writeonly restrict buffer DrawBuffer { + DrawCommand cmd_buffer[]; +}; +#endif + +// Per section, the last frame the occlusion pass saw it (low 31 bits) and +// whether it saw it the frame before (bit 31); `VISIBILITY_ACCESS` is +// `readonly` for the occlusion pass's vertex stage, which only reads it. +#ifdef VISIBILITY_BINDING +#ifndef VISIBILITY_ACCESS +#define VISIBILITY_ACCESS +#endif +layout(set = DISTANT_SET, binding = VISIBILITY_BINDING, std430) VISIBILITY_ACCESS restrict buffer VisibilityBuffer { + uint visibility_data[]; +}; +#endif + +// Per draw, its section's packed key. +#ifdef POSITION_BINDING +layout(set = DISTANT_SET, binding = POSITION_BINDING, std430) restrict buffer PositionBuffer { + uvec2 position_buffer[]; +}; +#endif + +#ifdef TRANSLUCENT_DATA_BINDING +layout(set = DISTANT_SET, binding = TRANSLUCENT_DATA_BINDING, std430) restrict buffer TranslucentData { + uint translucent_data[]; +}; +#endif + +#ifdef DRAW_BUFFER_BINDING +// Dropped at or past `end`, its region's end: the counts run on past a full +// region, which the draws clamp to. +void write_draw(uint index, uint end, uint instance, uint offset, uint quad_count) { + if (index >= end) { + return; + } + DrawCommand cmd; + cmd.count = quad_count * 6u; + cmd.instance_count = 1u; + cmd.first_index = 0u; + cmd.base_vertex = int(offset) << 2; + cmd.base_instance = instance; + cmd_buffer[index] = cmd; +} +#endif diff --git a/pomme-client/src/renderer/shaders/distant_setup.frag b/pomme-client/src/renderer/shaders/distant_setup.frag new file mode 100644 index 000000000..43b8461c6 --- /dev/null +++ b/pomme-client/src/renderer/shaders/distant_setup.frag @@ -0,0 +1,18 @@ +#version 450 + +// Distant terrain's depth target starts at the near plane wherever the +// chunks drew something, so distant terrain draws only where they didn't. +// The depth alone marks those pixels, the swapchain depth having no stencil. + +layout(set = 0, binding = 0) uniform texture2D frame_depth; +layout(set = 0, binding = 1) uniform sampler depth_sampler; + +layout(location = 0) out vec4 out_color; + +void main() { + if (texelFetch(sampler2D(frame_depth, depth_sampler), ivec2(gl_FragCoord.xy), 0).r == 1.0) { + discard; + } + gl_FragDepth = 0.0; + out_color = vec4(0.0); +} diff --git a/pomme-client/src/renderer/shaders/distant_translucents.comp b/pomme-client/src/renderer/shaders/distant_translucents.comp new file mode 100644 index 000000000..1b1ee9bbc --- /dev/null +++ b/pomme-client/src/renderer/shaders/distant_translucents.comp @@ -0,0 +1,30 @@ +#version 450 + +// The translucent draws placed by their distance bucket's prefix sum, so the +// list runs far to near. + +#define DISTANT_SET 0 +#define SCENE_BINDING 0 +#define DRAW_BUFFER_BINDING 1 +#define DRAW_COUNTS_BINDING 2 +#define SECTION_METADATA_BINDING 3 +#define RENDER_LIST_BINDING 4 +#define TRANSLUCENT_DATA_BINDING 5 +#include "distant_section.glsl" + +layout(local_size_x = LIST_GROUP) in; + +void main() { + if (gl_GlobalInvocationID.x >= min(translucent_draw_count, TRANSLUCENT_DRAWS)) { + return; + } + uint draw_id = translucent_data[gl_GlobalInvocationID.x + TRANSLUCENT_BUCKETS]; + SectionMeta meta = section_data[render_list[draw_id]]; + uint detail = section_detail(meta); + uvec3 rel = abs(section_position(meta) - (camera_section >> detail)); + uint distance = (rel.x + rel.y + rel.z) << detail; + distance = (TRANSLUCENT_BUCKETS - 1u) - min(distance, TRANSLUCENT_BUCKETS - 1u); + uint draw_ptr = atomicAdd(translucent_data[distance], 1u) + TRANSLUCENT_OFFSET; + write_draw(draw_ptr, TRANSLUCENT_END, draw_id, section_quad_start(meta), + meta.b.x & 0xFFFFu); +} diff --git a/pomme-client/src/renderer/shaders/distant_traversal.comp b/pomme-client/src/renderer/shaders/distant_traversal.comp new file mode 100644 index 000000000..69e22c200 --- /dev/null +++ b/pomme-client/src/renderer/shaders/distant_traversal.comp @@ -0,0 +1,356 @@ +#version 450 + +// One iteration of the walk down the node tree, a level at a time. Each +// listed node is frustum and Hi-Z tested; a node still larger on screen than +// the split size steps into its children (or asks for them), the rest draw. +// Nodes hidden by the Hi-Z or just outside the view are walked and asked for +// all the same, only not drawn, so what a turn or a climb brings into view +// is already at the detail it needs (a departure: the reference walks only +// what it draws). The nodes pushed for the next iteration size its dispatch: +// a workgroup per 1 << TRAVERSAL_GROUP_BITS of them, capped at the queue's +// room (another departure: the reference adds a workgroup per push and +// writes past a full queue). + +#define OUT_OF_BOUNDS 0xFFFFFFFFu + +#define DISTANT_SET 0 +#define SCENE_BINDING 0 +#define RENDER_LIST_BINDING 2 +#define RENDER_LIST_ACCESS +#include "distant_section.glsl" + +#define LOCAL_SIZE (1 << TRAVERSAL_GROUP_BITS) +layout(local_size_x = LOCAL_SIZE) in; + +// The nodes whose children the CPU should make, by packed key, after their +// count and the count of those out of sight among them. +layout(set = 0, binding = 1, std430) restrict buffer RequestQueue { + uvec2 request_index; + uvec2 request_queue[]; +}; + +// Per node: the key, the mesh (24 bits) under the flags' low byte, the first +// child (24 bits) under the flags' high byte. +layout(set = 0, binding = 3, std430) restrict buffer NodeData { + uvec4 nodes[]; +}; + +// Per iteration, its dispatch size (`xyz`) and how many nodes it has. +layout(set = 0, binding = 4, std430) restrict buffer QueueMeta { + uvec4 queue_meta[LEVELS]; +}; + +layout(set = 0, binding = 5, std430) readonly restrict buffer QueueSource { + uint queue_source[]; +}; + +layout(set = 0, binding = 6, std430) writeonly restrict buffer QueueSink { + uint queue_sink[]; +}; + +layout(set = 0, binding = 7) uniform texture2D hiz; +layout(set = 0, binding = 8) uniform sampler hiz_sampler; + +// Per node, the frame it was last drawn, for the cleaner. +layout(set = 0, binding = 9, std430) writeonly restrict buffer LastRenderFrame { + uint last_render_frame[]; +}; + +layout(push_constant) uniform TraversalPc { + uint queue_index; +}; + +struct Node { + uint id; + uvec2 raw_pos; + ivec3 pos; + uint level; + uint flags; + uint mesh; + uint child_ptr; +}; + +Node unpack_node(uint id) { + uvec4 packed = nodes[id]; + Node node; + node.id = id; + node.raw_pos = packed.xy; + node.level = lod_level(packed.xy); + node.pos = lod_position(packed.xy); + node.mesh = packed.z & 0xFFFFFFu; + node.child_ptr = packed.w & 0xFFFFFFu; + node.flags = (packed.z >> 24u) | ((packed.w >> 24u) << 8u); + return node; +} + +bool has_mesh(Node node) { + return node.mesh != NULL_ID; +} + +bool has_children(Node node) { + return node.child_ptr != NULL_ID; +} + +bool has_requested(Node node) { + return (node.flags & 1u) != 0u; +} + +uint child_count(Node node) { + return ((node.flags >> 2u) & 7u) + 1u; +} + +void mark_requested(inout Node node) { + node.flags |= 1u; + nodes[node.id].z |= 1u << 24u; +} + +// This iteration's node for the invocation, or none past the queue's count +// or room (the scratch queues', which the top-level ids never pass). +uint current_node() { + if (min(queue_meta[queue_index].w, MAX_LISTED) <= gl_GlobalInvocationID.x) { + return OUT_OF_BOUNDS; + } + return queue_source[gl_GlobalInvocationID.x]; +} + +uint push_index; + +uint workgroups(uint nodes) { + return (min(nodes, MAX_LISTED) + LOCAL_SIZE - 1u) >> TRAVERSAL_GROUP_BITS; +} + +// Room for `count` nodes in the next iteration, its dispatch grown by the +// workgroups they add; the pushes' ranges tile the queue, so the sizes sum +// to its workgroups. +void push_nodes_init(uint count) { + // A coarser node than the iteration expects (a record not yet rewritten) + // has no next iteration to push to. + if (queue_index + 1u >= LEVELS) { + push_index = MAX_LISTED; + return; + } + uint index = atomicAdd(queue_meta[queue_index + 1u].w, count); + uint added = workgroups(index + count) - workgroups(index); + if (added != 0u) { + atomicAdd(queue_meta[queue_index + 1u].x, added); + } + push_index = index; +} + +// Past the queue's room, dropped. +void push_node(uint id) { + if (push_index < MAX_LISTED) { + queue_sink[push_index] = id; + } + push_index++; +} + +// Frustum test against the near five planes; the far plane never cuts. +bool test_plane(vec4 plane, vec3 base, float size) { + return dot(plane.xyz, base + mix(vec3(size), vec3(0.0), lessThan(plane.xyz, vec3(0.0)))) >= -plane.w; +} + +bool outside_frustum(vec4 planes[6], vec3 base, float size) { + return !(test_plane(planes[0], base, size) && test_plane(planes[1], base, size) + && test_plane(planes[2], base, size) && test_plane(planes[3], base, size) + && test_plane(planes[4], base, size)); +} + +// The node's box projected, its screen bounds and the summed area of its +// faces; whether it lies outside the view, and outside the wider view that +// is still requested. +vec3 min_bb = vec3(0.0); +vec3 max_bb = vec3(0.0); +bool frustum_culled = false; +bool request_culled = false; +float screen_size = 0.0; + +// The node's corner relative to the camera's section, and to the camera. +vec3 section_relative(Node node) { + return vec3(((node.pos << node.level) - camera_section) << 5); +} + +float cross_mag(vec2 a, vec2 b) { + return abs(a.x * b.y - b.x * a.y); +} + +// NDC to the viewport's 0..1, depth as is. +vec3 to_screen(vec4 clip) { + vec3 ndc = clip.xyz / clip.w; + return vec3(ndc.xy * 0.5 + 0.5, ndc.z); +} + +void setup_screenspace(Node node) { + vec3 base = section_relative(node); + vec3 eye_base = base - camera_sub_pos; + float size = float(32 << node.level); + frustum_culled = outside_frustum(frustum, eye_base, size); + request_culled = frustum_culled && outside_frustum(request_frustum, eye_base, size); + if (request_culled) { + return; + } + vec4 P000 = distant_view_proj * vec4(base, 1.0); + mat3x4 axis = mat3x4(distant_view_proj) * size; + vec4 P100 = axis[0] + P000; + vec4 P001 = axis[2] + P000; + vec4 P101 = axis[2] + P100; + vec4 P010 = axis[1] + P000; + vec4 P110 = axis[1] + P100; + vec4 P011 = axis[1] + P001; + vec4 P111 = axis[1] + P101; + + vec3 p000 = to_screen(P000); + vec3 p100 = to_screen(P100); + vec3 p001 = to_screen(P001); + vec3 p101 = to_screen(P101); + vec3 p010 = to_screen(P010); + vec3 p110 = to_screen(P110); + vec3 p011 = to_screen(P011); + vec3 p111 = to_screen(P111); + + // Both the near and the far corner's three faces, so halved. + float area = 0.0; + { + vec2 A = p100.xy - p000.xy; + vec2 B = p010.xy - p000.xy; + vec2 C = p001.xy - p000.xy; + area += cross_mag(A, B) + cross_mag(A, C) + cross_mag(C, B); + } + { + vec2 A = p011.xy - p111.xy; + vec2 B = p101.xy - p111.xy; + vec2 C = p110.xy - p111.xy; + area += cross_mag(A, B) + cross_mag(A, C) + cross_mag(C, B); + } + screen_size = area * 0.5; + + min_bb = min(min(min(p000, p100), min(p001, p101)), min(min(p010, p110), min(p011, p111))); + max_bb = max(max(max(p000, p100), max(p001, p101)), max(max(p010, p110), max(p011, p111))); + min_bb = clamp(min_bb, vec3(0.0), vec3(1.0)); + max_bb = clamp(max_bb, vec3(0.0), vec3(1.0)); +} + +// Hidden when the farthest Hi-Z texel the box covers lies before the box's +// nearest point, at the level where the box spans about two texels. +bool culled_by_hiz() { + // A box over the whole screen is never culled. + if (any(lessThan(abs(max_bb.xy - min_bb.xy - vec2(1.0)), vec2(0.000001)))) { + return false; + } + ivec2 hiz_extent = ivec2(hiz_size >> 16u, hiz_size & 0xFFFFu); + vec2 size = (max_bb.xy - min_bb.xy) * hiz_extent; + float level = floor(log2(max(max(size.x, size.y), 1.0))) - 1.0; + level = clamp(level, 0.0, float(textureQueryLevels(sampler2D(hiz, hiz_sampler)) - 1)); + int mip = int(level); + hiz_extent = max(ivec2(1), hiz_extent >> mip); + ivec2 high = min(ivec2(ceil(max_bb.xy * hiz_extent)), hiz_extent - 1); + ivec2 low = ivec2(floor(min_bb.xy * hiz_extent)); + float farthest = -1.0; + for (int x = low.x; x <= high.x; x++) { + for (int y = low.y; y <= high.y; y++) { + farthest = max(texelFetch(sampler2D(hiz, hiz_sampler), ivec2(x, y), mip).r, farthest); + } + } + return farthest <= min_bb.z; +} + +bool should_descend() { + return screen_size > min_sss; +} + +// Asks the CPU for the node's children, once per node and up to the frame's +// request limit; nodes out of sight get at most a third of it, so what is in +// view refines first. +void add_request(inout Node node, bool visible) { + if (has_requested(node) || request_index.x >= request_limit) { + return; + } + if (!visible && atomicAdd(request_index.y, 1u) >= request_limit / 3u) { + return; + } + uint index = atomicAdd(request_index.x, 1u); + if (index < MAX_REQUESTS) { + request_queue[index] = node.raw_pos; + mark_requested(node); + } +} + +void enqueue_children(Node node) { + if (!has_children(node) || node.child_ptr == EMPTY_ID) { + return; + } + uint count = child_count(node); + push_nodes_init(count); + for (uint i = 0u; i < count; i++) { + push_node(node.child_ptr + i); + } +} + +void enqueue_render(Node node) { + if (section_count < MAX_LISTED) { + last_render_frame[node.id] = frame_id; + if (node.mesh != EMPTY_ID) { + uint index = atomicAdd(section_count, 1u); + if (index < MAX_LISTED) { + render_list[index] = node.mesh; + } + } + } +} + +vec3 closest_point(vec3 eye_relative, float scale) { + return mix(mix(vec3(0.0), eye_relative, greaterThan(eye_relative, vec3(0.0))), + eye_relative + scale, lessThan(eye_relative + scale, vec3(0.0))); +} + +vec3 furthest_point(vec3 eye_relative, float scale) { + return mix(eye_relative + scale, mix(eye_relative, vec3(0.0), greaterThan(eye_relative, vec3(0.0))), + lessThan(eye_relative + scale, vec3(0.0))); +} + +bool within_render_distance(Node node) { + vec3 close = closest_point(section_relative(node) - camera_sub_pos, float(32 << node.level)); + return close.x * close.x + close.z * close.z <= render_distance; +} + +void traverse(inout Node node) { + setup_screenspace(node); + if (request_culled) { + return; + } + bool visible = !frustum_culled && !culled_by_hiz(); + if (node.level != 0u && should_descend()) { + if (has_children(node)) { + enqueue_children(node); + } else { + add_request(node, visible); + // Drawn meanwhile when wholly within reach, with a buffer room of + // 16 cubed. + vec3 far = furthest_point(section_relative(node) - camera_sub_pos, float(32 << node.level)); + if (visible && far.x * far.x + far.z * far.z + 16.0 * 16.0 * 16.0 <= render_distance) { + enqueue_render(node); + } + } + } else if (has_mesh(node)) { + if (visible) { + enqueue_render(node); + } + } else { + // Wanted but not meshed yet: ask, and draw the children meanwhile. + add_request(node, visible); + if (node.level != 0u) { + enqueue_children(node); + } + } +} + +void main() { + uint id = current_node(); + if (id == OUT_OF_BOUNDS) { + return; + } + Node node = unpack_node(id); + if (within_render_distance(node)) { + traverse(node); + } +} diff --git a/pomme-client/src/renderer/shaders/distant_visibility_set.comp b/pomme-client/src/renderer/shaders/distant_visibility_set.comp new file mode 100644 index 000000000..70a96ce68 --- /dev/null +++ b/pomme-client/src/renderer/shaders/distant_visibility_set.comp @@ -0,0 +1,31 @@ +#version 450 + +// The listed nodes' last render frame set to now (bit 31 of the id: just +// allocated) or cleared (freed). + +#include "distant_consts.glsl" + +layout(local_size_x = LIST_GROUP) in; + +layout(set = 0, binding = 0, std430) writeonly restrict buffer LastRenderFrame { + uint last_render_frame[]; +}; + +layout(set = 0, binding = 1, std430) readonly restrict buffer Ids { + uint ids[]; +}; + +layout(push_constant) uniform SetPc { + uint count; + uint set_to; +}; + +void main() { + uint i = gl_GlobalInvocationID.x; + if (count <= i) { + return; + } + uint id = ids[i]; + bool reset = (id & VISIBILITY_RESET_BIT) != 0u; + last_render_frame[id & ~VISIBILITY_RESET_BIT] = reset ? set_to : 0xFFFFFFFFu; +} diff --git a/pomme-client/src/renderer/util.rs b/pomme-client/src/renderer/util.rs index b2e199b53..76f6fd93c 100644 --- a/pomme-client/src/renderer/util.rs +++ b/pomme-client/src/renderer/util.rs @@ -52,6 +52,50 @@ pub fn sampled_texture_desc(label: &str, width: u32, height: u32) -> rhi::Textur ) } +/// A compute pipeline with one bind group, its layouts, and a set per frame +/// in flight (or one, when nothing it binds is per frame). +pub struct ComputePass { + pub pipeline: rhi::backend::ComputePipeline, + layout: rhi::backend::PipelineLayout, + group_layout: rhi::backend::BindGroupLayout, + sets: Vec, +} + +impl ComputePass { + /// `bindings(frame)` gives the set for each of `frames`. + pub fn new( + device: &rhi::backend::Device, + label: &str, + shader: &[u8], + entries: &[rhi::BindGroupLayoutEntry], + push: Option, + frames: usize, + bindings: impl Fn(usize) -> Vec<(u32, rhi::backend::BindingResource)>, + ) -> Self { + let group_layout = rhi::backend::create_bind_group_layout(device, entries); + let layout = rhi::backend::create_pipeline_layout(device, &[group_layout], push); + Self { + pipeline: rhi::backend::create_compute_pipeline(device, layout, label, shader), + layout, + group_layout, + sets: (0..frames) + .map(|frame| device.create_bind_group(group_layout, &bindings(frame))) + .collect(), + } + } + + pub fn set(&self, frame: usize) -> rhi::backend::BindGroup { + self.sets[frame.min(self.sets.len() - 1)] + } + + pub fn destroy(&mut self, device: &rhi::backend::Device) { + destroy_bind_groups(device, self.sets.drain(..)); + self.pipeline.destroy(device); + self.layout.destroy(device); + self.group_layout.destroy(device); + } +} + /// One host-visible buffer per frame in flight. pub fn create_per_frame_host_buffers( device: &rhi::backend::Device, @@ -92,6 +136,30 @@ pub fn texture_binding( ] } +/// A storage buffer at `binding`. +pub fn storage_binding( + binding: u32, + buffer: &rhi::backend::Buffer, + bytes: u64, +) -> (u32, rhi::backend::BindingResource) { + ( + binding, + rhi::backend::BindingResource::storage(buffer, bytes), + ) +} + +/// A uniform buffer at `binding`. +pub fn uniform_binding( + binding: u32, + buffer: &rhi::backend::Buffer, + bytes: u64, +) -> (u32, rhi::backend::BindingResource) { + ( + binding, + rhi::backend::BindingResource::uniform(buffer, bytes), + ) +} + /// A [`FRAGMENT_TEXTURE`] group pointed at `texture`. pub fn create_texture_group( device: &rhi::backend::Device, @@ -114,12 +182,7 @@ pub fn create_uniform_sets( let buffers = create_per_frame_host_buffers(device, size, rhi::BufferUsages::UNIFORM, name); let groups = buffers .iter() - .map(|buffer| { - device.create_bind_group( - layout, - &[(0, rhi::backend::BindingResource::uniform(buffer, size))], - ) - }) + .map(|buffer| device.create_bind_group(layout, &[uniform_binding(0, buffer, size)])) .collect(); (groups, buffers) } diff --git a/pomme-client/src/singleplayer.rs b/pomme-client/src/singleplayer.rs index d2a13f007..0eb98eff7 100644 --- a/pomme-client/src/singleplayer.rs +++ b/pomme-client/src/singleplayer.rs @@ -18,7 +18,7 @@ pub const UNAVAILABLE_MESSAGE: &str = "This build was compiled without singlepla #[cfg(feature = "singleplayer")] mod steel { use std::fs::{File, OpenOptions, TryLockError}; - use std::path::Path; + use std::path::{Path, PathBuf}; use pomme_singleplayer::{ Difficulty, GameType, LaunchOptions, PendingWorld, Progress, ServerTransport, WorldHandle, @@ -67,6 +67,7 @@ mod steel { // released, not after. server: Server, _lock: SessionLock, + dir: PathBuf, } impl World { @@ -108,6 +109,11 @@ mod steel { pub fn is_closed(&self) -> bool { self.handle().is_none_or(WorldHandle::is_finished) } + + /// The save folder. + pub fn dir(&self) -> &Path { + &self.dir + } } /// Starts `world` and hands back the client's end of the pipe. @@ -149,6 +155,7 @@ mod steel { World { server: Server::Starting(pending), _lock: lock, + dir: dir.to_path_buf(), }, client, )) @@ -178,6 +185,10 @@ mod steel { pub const fn is_closed(&self) -> bool { match *self {} } + + pub const fn dir(&self) -> &Path { + match *self {} + } } pub fn open( diff --git a/pomme-client/src/ui/hud.rs b/pomme-client/src/ui/hud.rs index d08e8b67f..aa1303bda 100644 --- a/pomme-client/src/ui/hud.rs +++ b/pomme-client/src/ui/hud.rs @@ -28,6 +28,7 @@ pub enum ContextualBarKind<'a> { } pub struct FrameTimings { + pub gpu: Vec<(&'static str, f32)>, pub pace_ms: f32, pub frame_ms: f32, pub fence_ms: f32, @@ -383,6 +384,7 @@ pub struct DebugInfo<'a> { )>, pub chunk_count: u32, pub sections_drawn: u32, + pub triangles_drawn: u32, pub occlusion_on: bool, /// Mesh-scheduling tiers (visible, margin, hidden) of loaded columns when /// the visibility gate is active; `None` while it falls back to meshing @@ -1648,9 +1650,10 @@ pub fn build_debug_overlay( String::new(), format!("Chunks: {} loaded", info.chunk_count), format!( - "Sections drawn: {} (occlusion {})", + "Sections drawn: {} (occlusion {}), {}k triangles", info.sections_drawn, - if info.occlusion_on { "on" } else { "off" } + if info.occlusion_on { "on" } else { "off" }, + info.triangles_drawn / 1000 ), match info.mesh_gate { Some((vis, margin, hidden)) => { @@ -1688,6 +1691,13 @@ pub fn build_debug_overlay( right_lines.push(format!(" Cull: {:.2}ms", t.cull_ms)); right_lines.push(format!(" Draw: {:.2}ms", t.draw_ms)); right_lines.push(format!(" Present: {:.2}ms", t.present_ms)); + if !t.gpu.is_empty() { + let gpu_total: f32 = t.gpu.iter().map(|(_, ms)| ms).sum(); + right_lines.push(format!("GPU: {gpu_total:.2}ms")); + } + for (name, ms) in &t.gpu { + right_lines.push(format!(" {name}: {ms:.2}ms")); + } } let right_x = info.screen_w as f32 - pad; push_debug_lines( diff --git a/pomme-client/src/ui/menu/mod.rs b/pomme-client/src/ui/menu/mod.rs index b412cde2a..cadcf1fc3 100644 --- a/pomme-client/src/ui/menu/mod.rs +++ b/pomme-client/src/ui/menu/mod.rs @@ -40,6 +40,8 @@ struct Settings { // of 8 move to Off too. #[serde(rename = "chunk_detail_v2", default = "default_chunk_detail")] chunk_detail: u32, + #[serde(default)] + distant_terrain: u32, simulation_distance: u32, #[serde(default = "default_fov")] fov: u32, @@ -149,6 +151,30 @@ const CHUNK_DETAIL_MIN: u32 = 8; pub const CHUNK_DETAIL_OFF: u32 = 49; const CHUNK_DETAIL_SPAN: f32 = (CHUNK_DETAIL_OFF - CHUNK_DETAIL_MIN) as f32; +/// The Distant Terrain slider's steps past Off: 32 to 256 chunks, 16 apart. +const DISTANT_TERRAIN_STEPS: u32 = 15; +const DISTANT_TERRAIN_STEP: u32 = 16; + +/// The Distant Terrain slider step nearest `chunks`: 0 for off, else +/// `1..=DISTANT_TERRAIN_STEPS`. +fn distant_terrain_step(chunks: u32) -> u32 { + if chunks == 0 { + return 0; + } + ((chunks + DISTANT_TERRAIN_STEP / 2) / DISTANT_TERRAIN_STEP) + .saturating_sub(1) + .clamp(1, DISTANT_TERRAIN_STEPS) +} + +/// The chunks Distant Terrain slider step `step` draws out to. +fn distant_terrain_chunks(step: u32) -> u32 { + if step == 0 { + 0 + } else { + DISTANT_TERRAIN_STEP * (step + 1) + } +} + fn default_fov_effect_scale() -> f32 { 1.0 } @@ -220,6 +246,7 @@ impl Default for Settings { gui_scale: 0, render_distance: 12, chunk_detail: CHUNK_DETAIL_OFF, + distant_terrain: 0, simulation_distance: 12, fov: 70, fov_effect_scale: 1.0, @@ -719,6 +746,9 @@ pub struct MainMenu { /// beyond it. Pomme-custom: vanilla has no LOD, this buys its look back /// within a VRAM budget. pub chunk_detail: u32, + /// How far distant terrain draws, in chunks, 0 for off. Pomme-custom: + /// the chunks the server forgets, drawn coarsely past the loaded ones. + pub distant_terrain: u32, pub simulation_distance: u32, /// Server-announced view distance cap; 0 when unknown (slider runs 2..32). pub server_render_distance: u32, @@ -881,6 +911,7 @@ impl MainMenu { gui_scale_setting: settings.gui_scale, render_distance: settings.render_distance, chunk_detail: settings.chunk_detail, + distant_terrain: distant_terrain_chunks(distant_terrain_step(settings.distant_terrain)), simulation_distance: settings.simulation_distance, server_render_distance: 0, fov: settings.fov, @@ -1133,6 +1164,7 @@ impl MainMenu { .saved() .unwrap_or(self.render_distance), chunk_detail: self.chunk_detail, + distant_terrain: self.distant_terrain, simulation_distance: self.simulation_distance, fov: self.fov, fov_effect_scale: self.fov_effect_scale, @@ -1205,6 +1237,13 @@ impl MainMenu { } } + /// Sets Distant Terrain to `chunks`, 0 for off, else snapped to the + /// slider's steps, as a script asks. + #[cfg(feature = "test-harness")] + pub(crate) fn set_distant_terrain(&mut self, chunks: u32) { + self.distant_terrain = distant_terrain_chunks(distant_terrain_step(chunks)); + } + pub fn set_menu_background_blurriness(&mut self, radius: u8) { self.menu_background_blurriness = radius.min(MAX_MENU_BACKGROUND_BLURRINESS); self.settings_dirty = true; @@ -1671,6 +1710,17 @@ mod tests { assert_eq!(settled(&mut menu), both); } + #[test] + fn distant_terrain_snaps_to_its_slider_steps() { + assert_eq!(distant_terrain_step(0), 0); + assert_eq!(distant_terrain_chunks(distant_terrain_step(8)), 32); + assert_eq!(distant_terrain_chunks(distant_terrain_step(100)), 96); + assert_eq!(distant_terrain_chunks(distant_terrain_step(1000)), 256); + for step in 0..=DISTANT_TERRAIN_STEPS { + assert_eq!(distant_terrain_step(distant_terrain_chunks(step)), step); + } + } + #[test] fn saves_from_before_chunk_detail_off_load_as_off() { let mut json = serde_json::to_value(Settings::default()).unwrap(); diff --git a/pomme-client/src/ui/menu/options.rs b/pomme-client/src/ui/menu/options.rs index 42d2e1a51..57b649790 100644 --- a/pomme-client/src/ui/menu/options.rs +++ b/pomme-client/src/ui/menu/options.rs @@ -50,6 +50,7 @@ const CHAT_SCALE: &str = "Chat Text Size:"; const CHAT_LINE_SPACING: &str = "Line Spacing:"; const CHAT_DELAY: &str = "Chat Delay:"; const MENU_BACKGROUND_BLUR: &str = "Menu Background Blur:"; +const DISTANT_TERRAIN: &str = "Distant Terrain:"; /// Vanilla `genericValueOrOffLabel`. fn menu_background_blur_label(radius: u8) -> String { @@ -228,6 +229,7 @@ impl MainMenu { Some(match prefix { "Render Distance:" => self.render_distance_max() as f32 - 2.0, "Chunk Detail:" => super::CHUNK_DETAIL_SPAN, + DISTANT_TERRAIN => super::DISTANT_TERRAIN_STEPS as f32, "Simulation Distance:" => 27.0, "Max Framerate:" => 25.0, "FOV:" => 80.0, @@ -242,14 +244,11 @@ impl MainMenu { .map_or(gs / (w - 8.0 * gs), |span| 1.0 / span) } - /// Upper bound of the render distance slider: the server-announced view - /// distance while connected (can exceed 32), 32 otherwise. + /// Upper bound of the render distance slider: vanilla's 32, or past it + /// to a server's larger view distance (Pomme-only). The server's own + /// value caps only the distance drawn, as in vanilla. pub(crate) fn render_distance_max(&self) -> u32 { - if self.server_render_distance > 0 { - self.server_render_distance.max(3) - } else { - 32 - } + self.server_render_distance.max(32) } pub(super) fn build_options_video( @@ -265,16 +264,17 @@ impl MainMenu { DisplayMode::Borderless => "Fullscreen: Borderless", DisplayMode::Fullscreen => "Fullscreen: Exclusive", }; - let rd_max = self.render_distance_max(); - let rd = format!( - "Render Distance: {} chunks", - self.render_distance.min(rd_max) - ); + let rd = format!("Render Distance: {} chunks", self.render_distance); let cd = if self.chunk_detail >= super::CHUNK_DETAIL_OFF { "Chunk Detail: Off".to_string() } else { format!("Chunk Detail: {} chunks", self.chunk_detail) }; + let dt = if self.distant_terrain == 0 { + format!("{DISTANT_TERRAIN} OFF") + } else { + format!("{DISTANT_TERRAIN} {} chunks", self.distant_terrain) + }; let sd = format!("Simulation Distance: {} chunks", self.simulation_distance); let mf = if self.max_framerate >= super::MAX_FRAMERATE_UNLIMITED { "Max Framerate: Unlimited".to_string() @@ -327,7 +327,7 @@ impl MainMenu { // TODO: static stub, not wired (see mesher::enqueue). OptRow::Pair("Prioritize Chunk Updates: None", &sd), OptRow::Pair("Smooth Lighting: ON", &clouds_label), - OptRow::PairLeft(&cd), + OptRow::Pair(&cd, &dt), OptRow::Pair("Particles: All", "Mipmap Levels: 4"), OptRow::Pair(entity_shadows_label, "Entity Distance: 100%"), OptRow::Pair(&blur_label, "Cloud Range: 128"), @@ -338,15 +338,20 @@ impl MainMenu { OptRow::Pair(autosave_label, vignette_label), OptRow::Pair(&attack_label, "Chunk Fade-in: 1.0s"), ]; - let rd_frac = ((self.render_distance as f32 - 2.0) / (rd_max as f32 - 2.0)).clamp(0.0, 1.0); + let rd_frac = ((self.render_distance as f32 - 2.0) + / (self.render_distance_max() as f32 - 2.0)) + .clamp(0.0, 1.0); let cd_frac = (self.chunk_detail.saturating_sub(super::CHUNK_DETAIL_MIN) as f32 / super::CHUNK_DETAIL_SPAN) .clamp(0.0, 1.0); + let dt_frac = super::distant_terrain_step(self.distant_terrain) as f32 + / super::DISTANT_TERRAIN_STEPS as f32; let sd_frac = (self.simulation_distance as f32 - 5.0) / 27.0; let mf_frac = (self.max_framerate as f32 - 10.0) / 250.0; let sliders: &[(&str, f32)] = &[ ("Render Distance:", rd_frac), ("Chunk Detail:", cd_frac), + (DISTANT_TERRAIN, dt_frac), ("Simulation Distance:", sd_frac), ("Max Framerate:", mf_frac), (BRIGHTNESS, self.brightness), @@ -1571,6 +1576,9 @@ impl MainMenu { "Chunk Detail:" => { self.chunk_detail = super::CHUNK_DETAIL_MIN + (v * span).round() as u32 } + DISTANT_TERRAIN => { + self.distant_terrain = super::distant_terrain_chunks((v * span).round() as u32) + } "Simulation Distance:" => { self.simulation_distance = (5.0 + v * span).round() as u32 } @@ -2098,6 +2106,17 @@ mod tests { assert!(option_enabled("Graphics Backend: Default", disabled)); } + #[test] + fn a_server_view_distance_widens_the_render_distance_slider_past_32_only() { + let mut key_bindings = crate::app::input::KeyBindings::default(); + let mut menu = super::super::test_menu("", &mut key_bindings); + assert_eq!(menu.render_distance_max(), 32); + menu.server_render_distance = 10; + assert_eq!(menu.render_distance_max(), 32); + menu.server_render_distance = 128; + assert_eq!(menu.render_distance_max(), 128); + } + #[test] fn brightness_label_names_its_ends_and_default() { assert_eq!(brightness_label(0.0), "Brightness: Moody"); diff --git a/pomme-client/src/util.rs b/pomme-client/src/util.rs index d6d3c7932..b3f141a84 100644 --- a/pomme-client/src/util.rs +++ b/pomme-client/src/util.rs @@ -90,6 +90,17 @@ pub fn write_atomic(path: &Path, bytes: &[u8]) -> io::Result<()> { result } +/// `rx`'s next message from a worker thread, `None` while none waits. The +/// thread only ends early by panicking, so this panics with `gone` then and +/// the game goes down with it. +pub fn try_recv_live(rx: &crossbeam_channel::Receiver, gone: &str) -> Option { + match rx.try_recv() { + Ok(message) => Some(message), + Err(crossbeam_channel::TryRecvError::Empty) => None, + Err(crossbeam_channel::TryRecvError::Disconnected) => panic!("{gone}"), + } +} + /// The Android app, for calls into its Java side (`open_url`). Replaced on /// each `android_main`, since a recreated activity gets a new one. #[cfg(target_os = "android")] diff --git a/pomme-client/src/world/block/mod.rs b/pomme-client/src/world/block/mod.rs index e78dd620e..2d35f3319 100644 --- a/pomme-client/src/world/block/mod.rs +++ b/pomme-client/src/world/block/mod.rs @@ -256,6 +256,41 @@ impl BlockStateData { self.is_air } + /// Vanilla `LeavesBlock`: every leaf block's id ends so. + pub(crate) fn is_leaves(&self) -> bool { + self.id.ends_with("_leaves") + } + + /// Vanilla `LiquidBlock`: the fluid itself, not a block holding one. + pub(crate) fn is_fluid_block(&self) -> bool { + matches!(self.id, "water" | "lava") + } + + /// Vanilla `skipRendering` against a neighbour of its own kind across + /// `direction`: `HalfTransparentBlock`, `LiquidBlock` and powder snow on + /// every side, mangrove roots only above and below. + pub(crate) fn culls_same(&self, direction: model::Direction) -> bool { + match self.id { + "mangrove_roots" => matches!(direction, model::Direction::Up | model::Direction::Down), + id => { + self.is_fluid_block() + || matches!( + id, + "glass" + | "tinted_glass" + | "ice" + | "frosted_ice" + | "blue_ice" + | "slime_block" + | "honey_block" + | "powder_snow" + ) + || id.ends_with("_stained_glass") + || id.ends_with("copper_grate") + } + } + } + /// `getRenderShape() == RenderShape.INVISIBLE`: the blocks overriding it. pub(crate) fn is_invisible(&self) -> bool { self.is_air @@ -1210,6 +1245,20 @@ mod tests { } } + #[test] + fn mangrove_roots_cull_their_own_kind_only_vertically() { + use model::Direction; + let table = BlockStateTable::native(); + let data = |name| table.block_state_data(table.first_state_of(name).unwrap()); + let roots = data("mangrove_roots"); + for direction in Direction::VALUES { + let vertical = matches!(direction, Direction::Up | Direction::Down); + assert_eq!(roots.culls_same(direction), vertical, "{direction:?}"); + assert!(data("glass").culls_same(direction)); + assert!(!data("stone").culls_same(direction)); + } + } + #[test] fn push_reactions_match_vanilla() { let table = BlockStateTable::native(); diff --git a/pomme-client/src/world/block/model.rs b/pomme-client/src/world/block/model.rs index 38d4d2814..43cc4d395 100644 --- a/pomme-client/src/world/block/model.rs +++ b/pomme-client/src/world/block/model.rs @@ -628,7 +628,6 @@ pub fn bake_all_models( if !results.contains_key(block_name) && !multipart_results.contains_key(block_name) && !crate::world::block_entity::is_block_entity_block(block_name) - && !crate::world::block_entity::is_fluid_block(block_name) && !crate::world::block_entity::is_invisible_block(block_name) { missing_names.push(block_name.to_string()); diff --git a/pomme-client/src/world/block_entity.rs b/pomme-client/src/world/block_entity.rs index 38c89fd1d..812d43ad1 100644 --- a/pomme-client/src/world/block_entity.rs +++ b/pomme-client/src/world/block_entity.rs @@ -139,10 +139,6 @@ pub fn is_invisible_block(name: &str) -> bool { ) } -pub fn is_fluid_block(name: &str) -> bool { - matches!(name, "water" | "lava" | "bubble_column") -} - #[cfg(test)] mod tests { use super::*; diff --git a/pomme-client/src/world/chunk.rs b/pomme-client/src/world/chunk.rs index 680a2c111..60038f8a2 100644 --- a/pomme-client/src/world/chunk.rs +++ b/pomme-client/src/world/chunk.rs @@ -475,7 +475,7 @@ pub struct ChunkRing { slots: Box<[T]>, } -const RING_SIDE: i32 = MAX_VIEW_DISTANCE as i32 * 2 + 1; +pub(crate) const RING_SIDE: i32 = MAX_VIEW_DISTANCE as i32 * 2 + 1; /// A column's slot in a `side`×`side` square that wraps around the world. fn ring_index(pos: ChunkPos, side: i32) -> usize { @@ -685,30 +685,17 @@ impl ChunkStore { self.min_y } + /// The lowest section's y (vanilla `LevelHeightAccessor.getMinSectionY`). + pub fn min_section_y(&self) -> i32 { + self.min_y >> 4 + } + /// Number of 16³ block sections in a column (zero-based section index range /// `0..section_count`). pub fn section_count(&self) -> i32 { (self.height() / 16) as i32 } - /// Whether the block section at world section-y has only air (vanilla - /// `LevelChunkSection.hasOnlyAir`, from its block count). Missing chunks - /// and out-of-range sections read as empty. - pub fn section_is_empty(&self, pos: (i32, i32), section_y: i32) -> bool { - let Some(chunk) = self.get_chunk(&ChunkPos::new(pos.0, pos.1)) else { - return true; - }; - let index = section_y - (self.min_y() >> 4); - let chunk = chunk.read(); - match usize::try_from(index) - .ok() - .and_then(|i| chunk.sections.get(i)) - { - Some(section) => section.block_count == 0, - None => true, - } - } - /// Top non-motion-blocking Y for the column (vanilla MOTION_BLOCKING /// surface, i.e. one above the highest solid block). Used to position /// weather columns. Returns `min_y` when the chunk or its heightmap is diff --git a/pomme-client/src/world/light/mod.rs b/pomme-client/src/world/light/mod.rs index d9a23b6e5..08c928858 100644 --- a/pomme-client/src/world/light/mod.rs +++ b/pomme-client/src/world/light/mod.rs @@ -4,10 +4,11 @@ //! relights, and changed sections publish into the per-column //! [`ChunkLightData`] the mesher already snapshots. //! -//! Tick flow (vanilla `ClientLevel.update`): [`LevelLightEngine::poll_and_run`] -//! drains queued light tasks at vanilla's rate limit, then runs each engine's -//! update passes, publishing between the decrease and increase passes exactly -//! where vanilla's `swapSectionMap` sits. +//! The engine runs on its own thread ([`worker::LightWorker`]), which hands +//! it the game's calls in order and reports the lit columns back. Each run +//! applies the queued tasks, then runs each engine's update passes, +//! publishing between the decrease and increase passes exactly where +//! vanilla's `swapSectionMap` sits. mod block; mod data; @@ -16,6 +17,7 @@ mod queue; mod sky; mod sources; mod storage; +pub(crate) mod worker; mod world; use std::collections::{HashSet, VecDeque}; @@ -30,7 +32,9 @@ use sky::SkyLightEngine; use sources::ChunkSkyLightSources; use storage::StorageCore; pub(crate) use storage::{LightPos, SectionKey}; -use world::StoreWorld; +use worker::LightMap; +pub(crate) use worker::LightWorker; +use world::{LightBlockGetter, LightWorld}; use crate::world::block::{BlockStateData, BlockStateTable}; use crate::world::chunk::{ChunkLightData, ChunkStore}; @@ -144,7 +148,7 @@ pub(crate) fn section_entries( /// write plus its light hooks. Returns the previous state. pub(crate) fn set_block_and_light( store: &ChunkStore, - engine: &mut LevelLightEngine, + light: &mut LightWorker, block_state_table: BlockStateTable, x: i32, y: i32, @@ -152,14 +156,7 @@ pub(crate) fn set_block_and_light( state: BlockState, ) -> BlockState { let (old, empty_flip) = store.set_block_state_tracked(x, y, z, state, block_state_table); - engine.on_block_changed( - block_state_table, - LightPos::new(x, y, z), - old, - state, - empty_flip, - &|column_y| store.get_block_state(x, column_y, z), - ); + light.block_changed(LightPos::new(x, y, z), old, state, empty_flip); old } @@ -178,13 +175,16 @@ pub(crate) enum LightTask { Remove { pos: (i32, i32) }, } -/// Remesh requests produced by a light run. +/// What a light run changed for the game: remesh requests, and the columns +/// whose light came on or went off. #[derive(Default)] pub(crate) struct LightDirty { /// World-space light sections whose rendered light may have changed. pub sections: HashSet, /// Columns whose chunk-load light applied: remesh column + neighbors. pub columns: Vec<(i32, i32)>, + /// Vanilla `setLightEnabled` calls, in order. + pub enabled: Vec<((i32, i32), bool)>, } #[derive(Clone, Copy)] @@ -216,18 +216,6 @@ impl LevelLightEngine { } } - /// Vanilla `LevelLightEngine.lightOnInColumn`: the column's light has been - /// applied, in both layers the dimension has. Vanilla's renderer requires - /// it of a section's whole 3x3 neighbourhood before the section may first - /// compile (`SectionUpdateTracker.hasAllNeighbors`). - pub fn light_on_in_column(&self, column: (i32, i32)) -> bool { - self.block.storage.light_on_in_column(column) - && self - .sky - .as_ref() - .is_none_or(|sky| sky.storage.light_on_in_column(column)) - } - pub fn light_section_count(&self) -> usize { (self.section_count + 2) as usize } @@ -243,39 +231,20 @@ impl LevelLightEngine { /// Vanilla `LevelChunk.setBlockState`'s light lines: section empty-status /// first, then (when the light properties changed) the sky-source /// heightmap update followed by a relight check. Call with the block - /// already written to the chunk; `column_state` reads the block column at - /// the position's x/z by world Y. + /// already written to the chunk. pub fn on_block_changed( &mut self, - block_state_table: BlockStateTable, + world: &LightWorld, pos: LightPos, old: BlockState, new: BlockState, empty_flip: Option, - column_state: &impl Fn(i32) -> BlockState, ) { if let Some(empty) = empty_flip { - self.block.update_section_status(pos.section(), empty); - if let Some(sky) = &mut self.sky { - sky.update_section_status(pos.section(), empty); - } + self.update_section_status(pos.section(), empty); } - if has_different_light_properties(old, new, block_state_table) { - if let Some(sky) = &mut self.sky - && let Some(sources) = sky.sources.get_mut(&(pos.x >> 4, pos.z >> 4)) - { - sources.update( - block_state_table, - column_state, - pos.x & 15, - pos.y, - pos.z & 15, - ); - } - self.block.check_block(pos); - if let Some(sky) = &mut self.sky { - sky.check_block(pos); - } + if has_different_light_properties(old, new, world.block_state_table()) { + self.relight(world, pos); } } @@ -285,28 +254,33 @@ impl LevelLightEngine { /// `hasDifferentLightProperties` work-skip dropped — section status /// dedups and `checkNode` recomputes idempotently, so results match at a /// small recompute cost. - pub fn on_block_dirty( - &mut self, - store: &ChunkStore, - block_state_table: BlockStateTable, - x: i32, - y: i32, - z: i32, - ) { - let pos = LightPos::new(x, y, z); - let empty = store.section_is_empty((x >> 4, z >> 4), y >> 4); - self.block.update_section_status(pos.section(), empty); + pub fn on_block_dirty(&mut self, world: &LightWorld, pos: LightPos) { + let empty = world.section_is_empty((pos.x >> 4, pos.z >> 4), pos.y >> 4); + self.update_section_status(pos.section(), empty); + self.relight(world, pos); + } + + /// Both layers' section empty-status (vanilla `updateSectionStatus`). + fn update_section_status(&mut self, key: SectionKey, empty: bool) { + self.block.update_section_status(key, empty); if let Some(sky) = &mut self.sky { - sky.update_section_status(pos.section(), empty); - if let Some(sources) = sky.sources.get_mut(&(x >> 4, z >> 4)) { - sources.update( - block_state_table, - &|cy| store.get_block_state(x, cy, z), - x & 15, - y, - z & 15, - ); - } + sky.update_section_status(key, empty); + } + } + + /// The sky-source heightmap update and both layers' relight check for a + /// changed block. + fn relight(&mut self, world: &LightWorld, pos: LightPos) { + if let Some(sky) = &mut self.sky + && let Some(sources) = sky.sources.get_mut(&(pos.x >> 4, pos.z >> 4)) + { + sources.update( + world.block_state_table(), + &|column_y| world.state(LightPos::new(pos.x, column_y, pos.z)), + pos.x & 15, + pos.y, + pos.z & 15, + ); } self.block.check_block(pos); if let Some(sky) = &mut self.sky { @@ -321,9 +295,9 @@ impl LevelLightEngine { /// later publishes only need to touch changed sections. pub fn on_chunk_loaded( &mut self, - store: &mut ChunkStore, - block_state_table: BlockStateTable, + world: &LightWorld, pos: (i32, i32), + light_data: &mut LightMap, ) { let count = self.light_section_count(); let min_section_y = self.min_section_y; @@ -342,26 +316,26 @@ impl LevelLightEngine { .sky .column_top(pos) .map(|top| top - (self.min_section_y - 1)); - if let Some(chunk) = store.get_chunk(&crate::position::ChunkPos::new(pos.0, pos.1)) { + if let Some(chunk) = world.chunk(pos) { sky.sources.insert( pos, ChunkSkyLightSources::fill_from( - store.min_y(), + world.min_y(), &chunk.read(), - block_state_table, + world.block_state_table(), ), ); } } - store.light_data.insert( + light_data.insert( pos, - Arc::new(ChunkLightData { + ChunkLightData { sky_sections, block_sections, - min_y: store.min_y(), + min_y: world.min_y(), has_sky: self.sky.is_some(), sky_top_section, - }), + }, ); } @@ -374,32 +348,18 @@ impl LevelLightEngine { } } - /// Drains queued light tasks at vanilla's per-tick rate limit, then runs - /// the light updates. Call once per game tick. - pub fn poll_and_run( - &mut self, - store: &mut ChunkStore, - block_state_table: BlockStateTable, - out: &mut LightDirty, - ) { - // Vanilla ClientLevel.pollLightUpdates: size < 1000 ? max(10, size/10) - // : size tasks per tick. - let size = self.tasks.len(); - let quota = if size < 1000 { - (size / 10).max(10) - } else { - size - }; - for _ in 0..quota { - let Some(task) = self.tasks.pop_front() else { - break; - }; - self.apply_task(task, store, out); + /// Applies every queued light task, then runs the light updates. Vanilla + /// `ClientLevel.pollLightUpdates` meters the tasks per tick to spare its + /// render thread; off it, all of them apply at once, as vanilla's do once + /// a thousand are queued. + pub fn run(&mut self, world: &LightWorld, light_data: &mut LightMap, out: &mut LightDirty) { + while let Some(task) = self.tasks.pop_front() { + self.apply_task(task, world, out); } - self.run_light_updates(store, block_state_table, out); + self.run_light_updates(world, light_data, out); } - fn apply_task(&mut self, task: LightTask, store: &mut ChunkStore, out: &mut LightDirty) { + fn apply_task(&mut self, task: LightTask, world: &LightWorld, out: &mut LightDirty) { match task { LightTask::ApplyLight { pos, @@ -438,16 +398,13 @@ impl LevelLightEngine { if let Some(engine) = &mut self.sky { engine.set_light_enabled(pos, true); } + out.enabled.push((pos, true)); if enable { // Vanilla enableChunkLight: report each block section's // emptiness, then schedule the column's meshes. for section_y in self.min_section_y..self.min_section_y + self.section_count { let key = SectionKey::new(pos.0, section_y, pos.1); - let empty = store.section_is_empty(pos, section_y); - self.block.update_section_status(key, empty); - if let Some(engine) = &mut self.sky { - engine.update_section_status(key, empty); - } + self.update_section_status(key, world.section_is_empty(pos, section_y)); } out.columns.push(pos); } @@ -459,6 +416,7 @@ impl LevelLightEngine { if let Some(engine) = &mut self.sky { engine.set_light_enabled(pos, false); } + out.enabled.push((pos, false)); for index in 0..self.light_section_count() { let key = self.light_section_key(pos, index); self.block.queue_section_data(key, None); @@ -467,11 +425,7 @@ impl LevelLightEngine { } } for section_y in self.min_section_y..self.min_section_y + self.section_count { - let key = SectionKey::new(pos.0, section_y, pos.1); - self.block.update_section_status(key, true); - if let Some(engine) = &mut self.sky { - engine.update_section_status(key, true); - } + self.update_section_status(SectionKey::new(pos.0, section_y, pos.1), true); } } } @@ -482,46 +436,34 @@ impl LevelLightEngine { /// pass, before the increase pass. fn run_light_updates( &mut self, - store: &mut ChunkStore, - block_state_table: BlockStateTable, + world: &LightWorld, + light_data: &mut LightMap, out: &mut LightDirty, ) { let (min_section_y, section_count) = (self.min_section_y, self.section_count); - { - let world = StoreWorld::new(store, block_state_table); - self.block.begin_updates(&world); - } + self.block.begin_updates(world); Self::publish( &mut self.block.storage, None, - store, + light_data, LayerKind::Block, min_section_y, section_count, out, ); - { - let world = StoreWorld::new(store, block_state_table); - self.block.finish_updates(&world); - } + self.block.finish_updates(world); if let Some(sky) = &mut self.sky { - { - let world = StoreWorld::new(store, block_state_table); - sky.begin_updates(&world); - } + sky.begin_updates(world); Self::publish( &mut sky.storage, Some(&sky.sky), - store, + light_data, LayerKind::Sky, min_section_y, section_count, out, ); - { - let world = StoreWorld::new(store, block_state_table); - sky.finish_updates(&world); - } + sky.finish_updates(world); } } @@ -531,7 +473,7 @@ impl LevelLightEngine { fn publish( storage: &mut StorageCore, sky: Option<&sky::SkyStorage>, - store: &mut ChunkStore, + light_data: &mut LightMap, layer: LayerKind, min_section_y: i32, section_count: i32, @@ -542,7 +484,7 @@ impl LevelLightEngine { for key in changed { // Columns without chunk data have no ChunkLightData; their layers // republish via on_chunk_loaded when the chunk arrives. - let Some(column) = store.light_data.get_mut(&key.column()) else { + let Some(column) = light_data.get_mut(key.column()) else { continue; }; let index = key.y - (min_section_y - 1); diff --git a/pomme-client/src/world/light/worker.rs b/pomme-client/src/world/light/worker.rs new file mode 100644 index 000000000..1209a76ad --- /dev/null +++ b/pomme-client/src/world/light/worker.rs @@ -0,0 +1,396 @@ +//! The light thread. Vanilla's client runs `LevelLightEngine` on the render +//! thread; here it runs on its own, as vanilla's server engine does +//! (`ThreadedLevelLightEngine`), fed the same calls in the same order and +//! publishing the same sections. A departure for frame time: a burst of +//! chunk loads or unloads lights off the game thread instead of inside one +//! frame. Block edits still light within their frame when the thread is +//! idle, which it is unless such a burst is in progress: the game thread +//! waits a moment for them. Vanilla's per-tick metering of queued light +//! tasks is dropped: each run applies every task queued (see +//! [`LevelLightEngine::run`]). + +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; +use std::time::Duration; + +use azalea_block::BlockState; +use crossbeam_channel::{Receiver, RecvTimeoutError, Sender}; +use parking_lot::RwLock; + +use super::world::LightWorld; +use super::{LevelLightEngine, LightDirty, LightPos, LightTask}; +use crate::world::block::BlockStateTable; +use crate::world::chunk::{Chunk, ChunkLightData, ChunkStore}; + +/// How long the game thread waits for a block edit's light before it draws +/// the frame without it. +const EDIT_WAIT: Duration = Duration::from_millis(2); + +/// The thread only ends early by panicking; the game goes down with it. +const THREAD_GONE: &str = "the light thread panicked"; + +enum Message { + Task(LightTask), + ChunkLoaded { + pos: (i32, i32), + chunk: Arc>, + }, + ChunkUnloaded((i32, i32)), + BlockChanged { + pos: LightPos, + old: BlockState, + new: BlockState, + empty_flip: Option, + }, + BlockDirty(LightPos), +} + +/// One run's outcome: the columns whose light changed, and what to remesh. +struct Report { + /// Messages handled so far, so the game thread knows an edit's light is + /// in. + handled: u64, + columns: Vec<((i32, i32), Arc)>, + dirty: LightDirty, +} + +/// The thread's columns, and which changed since the last report. +#[derive(Default)] +pub(crate) struct LightMap { + columns: HashMap<(i32, i32), Arc>, + touched: HashSet<(i32, i32)>, +} + +impl LightMap { + pub fn get_mut(&mut self, column: (i32, i32)) -> Option<&mut Arc> { + let data = self.columns.get_mut(&column)?; + self.touched.insert(column); + Some(data) + } + + pub fn insert(&mut self, column: (i32, i32), data: ChunkLightData) { + self.columns.insert(column, Arc::new(data)); + self.touched.insert(column); + } + + pub fn remove(&mut self, column: (i32, i32)) { + self.columns.remove(&column); + self.touched.remove(&column); + } + + fn take_touched(&mut self) -> Vec<((i32, i32), Arc)> { + self.touched + .drain() + .filter_map(|column| Some((column, Arc::clone(self.columns.get(&column)?)))) + .collect() + } +} + +/// The game thread's handle: what it tells the engine, and what it learnt +/// back. +pub(crate) struct LightWorker { + tx: Sender, + rx: Receiver, + sent: u64, + /// The message count an edit's light is waited for up to. + awaited: Option, + /// Columns whose light is on (vanilla `lightOnInColumn`). + light_on: HashSet<(i32, i32)>, + /// Each column's last unload, by message count, until a report shows the + /// thread past it: what an earlier report says of the column is about the + /// chunk that went, not one loaded there since. + unloaded: HashMap<(i32, i32), u64>, + has_sky: bool, + section_count: i32, +} + +impl LightWorker { + pub fn spawn( + height: u32, + min_y: i32, + has_sky: bool, + block_state_table: BlockStateTable, + ) -> Self { + let (tx, messages) = crossbeam_channel::unbounded(); + let (reports, rx) = crossbeam_channel::unbounded(); + let engine = LevelLightEngine::new(height, min_y, has_sky); + std::thread::Builder::new() + .name("chunk-light".into()) + .spawn(move || { + run( + engine, + LightWorld::new(block_state_table, min_y), + &messages, + &reports, + ) + }) + .expect("failed to spawn the light thread"); + Self { + tx, + rx, + sent: 0, + awaited: None, + light_on: HashSet::new(), + unloaded: HashMap::new(), + has_sky, + section_count: (height / 16) as i32, + } + } + + /// Whether the dimension has sky light. + pub fn has_sky(&self) -> bool { + self.has_sky + } + + pub fn light_section_count(&self) -> usize { + (self.section_count + 2) as usize + } + + /// Vanilla `LevelLightEngine.lightOnInColumn`: the column's light has been + /// applied, in both layers the dimension has. Vanilla's renderer requires + /// it of a section's whole 3x3 neighbourhood before the section may first + /// compile (`SectionUpdateTracker.hasAllNeighbors`). + pub fn light_on_in_column(&self, column: (i32, i32)) -> bool { + self.light_on.contains(&column) + } + + fn send(&mut self, message: Message) { + self.sent += 1; + self.tx.send(message).expect(THREAD_GONE); + } + + pub fn queue_task(&mut self, task: LightTask) { + self.send(Message::Task(task)); + } + + /// A chunk column arrived: its blocks for the engine to read, before any + /// task about it. + pub fn chunk_loaded(&mut self, pos: (i32, i32), chunk: Arc>) { + self.send(Message::ChunkLoaded { pos, chunk }); + } + + /// Vanilla `queueLightRemoval`, and the sky sources go with the chunk. + pub fn chunk_unloaded(&mut self, pos: (i32, i32)) { + self.send(Message::ChunkUnloaded(pos)); + self.unloaded.insert(pos, self.sent); + } + + /// Vanilla `LevelChunk.setBlockState`'s light hooks, with the block + /// already written to the chunk. + pub fn block_changed( + &mut self, + pos: LightPos, + old: BlockState, + new: BlockState, + empty_flip: Option, + ) { + self.send_edit(Message::BlockChanged { + pos, + old, + new, + empty_flip, + }); + } + + /// Relight hook for positions mutated without old-state tracking (the + /// player-prediction paths, which apply and roll back edits internally). + pub fn block_dirty(&mut self, x: i32, y: i32, z: i32) { + self.send_edit(Message::BlockDirty(LightPos::new(x, y, z))); + } + + /// An edit's light is waited for, so it lands in the edit's frame. + fn send_edit(&mut self, message: Message) { + self.send(message); + self.awaited = Some(self.sent); + } + + /// Takes in what the thread lit since the last call: the columns' light + /// into the store, for the meshers, and the remesh scope into `out`. + /// Waits briefly for an edit's light, so it lands in the edit's frame. + pub fn poll(&mut self, store: &mut ChunkStore, out: &mut LightDirty) { + let mut apply = |this: &mut Self, report: Report| { + let current = |column: &(i32, i32)| { + this.unloaded + .get(column) + .is_none_or(|&seq| report.handled >= seq) + }; + for (column, data) in report.columns { + if current(&column) { + store.light_data.insert(column, data); + } + } + for (column, on) in report.dirty.enabled.into_iter().filter(|(c, _)| current(c)) { + if on { + this.light_on.insert(column); + } else { + this.light_on.remove(&column); + } + } + out.sections.extend( + report + .dirty + .sections + .into_iter() + .filter(|key| current(&key.column())), + ); + out.columns + .extend(report.dirty.columns.into_iter().filter(|c| current(c))); + this.unloaded.retain(|_, seq| *seq > report.handled); + if this.awaited.is_some_and(|seq| report.handled >= seq) { + this.awaited = None; + } + }; + while let Some(report) = crate::util::try_recv_live(&self.rx, THREAD_GONE) { + apply(self, report); + } + let deadline = std::time::Instant::now() + EDIT_WAIT; + while self.awaited.is_some() { + match self.rx.recv_deadline(deadline) { + Ok(report) => apply(self, report), + Err(RecvTimeoutError::Timeout) => break, + Err(RecvTimeoutError::Disconnected) => panic!("{THREAD_GONE}"), + } + } + } +} + +/// Ends when the game drops its [`LightWorker`]. +fn run( + mut engine: LevelLightEngine, + mut world: LightWorld, + messages: &Receiver, + reports: &Sender, +) { + let mut light_data = LightMap::default(); + let mut handled = 0u64; + for first in messages { + let mut out = LightDirty::default(); + for message in std::iter::once(first).chain(messages.try_iter()) { + handled += 1; + match message { + Message::Task(task) => engine.queue_task(task), + Message::ChunkLoaded { pos, chunk } => { + world.insert(pos, chunk); + engine.on_chunk_loaded(&world, pos, &mut light_data); + } + Message::ChunkUnloaded(pos) => { + world.remove(pos); + light_data.remove(pos); + engine.on_chunk_unloaded(pos); + engine.queue_task(LightTask::Remove { pos }); + } + Message::BlockChanged { + pos, + old, + new, + empty_flip, + } => engine.on_block_changed(&world, pos, old, new, empty_flip), + Message::BlockDirty(pos) => engine.on_block_dirty(&world, pos), + } + } + engine.run(&world, &mut light_data, &mut out); + let report = Report { + handled, + columns: light_data.take_touched(), + dirty: out, + }; + if reports.send(report).is_err() { + break; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::world::light::SectionKey; + + /// A worker with no thread behind it: the test sends its reports. + fn worker() -> (LightWorker, Receiver, Sender) { + let (tx, messages) = crossbeam_channel::unbounded(); + let (reports, rx) = crossbeam_channel::unbounded(); + let worker = LightWorker { + tx, + rx, + sent: 0, + awaited: None, + light_on: HashSet::new(), + unloaded: HashMap::new(), + has_sky: false, + section_count: 4, + }; + (worker, messages, reports) + } + + fn report(handled: u64, column: (i32, i32)) -> Report { + let data = ChunkLightData { + sky_sections: Vec::new(), + block_sections: Vec::new(), + min_y: 0, + has_sky: false, + sky_top_section: None, + }; + Report { + handled, + columns: vec![(column, Arc::new(data))], + dirty: LightDirty { + sections: HashSet::from([SectionKey::new(column.0, 0, column.1)]), + columns: vec![column], + enabled: vec![(column, true)], + }, + } + } + + #[test] + fn report_from_before_an_unload_skips_the_reloaded_column() { + let (mut worker, _messages, reports) = worker(); + let mut store = ChunkStore::new(2); + let column = (3, 4); + worker.queue_task(LightTask::Remove { pos: column }); + worker.chunk_unloaded(column); + // The reload's own messages follow; the thread hasn't seen any yet. + worker.queue_task(LightTask::Remove { pos: column }); + + reports.send(report(1, column)).unwrap(); + let mut out = LightDirty::default(); + worker.poll(&mut store, &mut out); + assert!(!store.light_data.contains_key(&column)); + assert!(out.sections.is_empty()); + assert!(out.columns.is_empty()); + assert!(!worker.light_on_in_column(column)); + + reports.send(report(3, column)).unwrap(); + worker.poll(&mut store, &mut out); + assert!(store.light_data.contains_key(&column)); + assert_eq!(out.sections.len(), 1); + assert_eq!(out.columns, [column]); + assert!(worker.light_on_in_column(column)); + assert!(worker.unloaded.is_empty()); + } + + #[test] + #[should_panic(expected = "light thread panicked")] + fn a_dead_thread_takes_the_game_down() { + let (mut worker, _messages, reports) = worker(); + drop(reports); + worker.poll(&mut ChunkStore::new(2), &mut LightDirty::default()); + } + + #[test] + fn an_edit_is_awaited_until_a_report_covers_it() { + let (mut worker, _messages, reports) = worker(); + let mut store = ChunkStore::new(2); + let mut out = LightDirty::default(); + worker.block_dirty(0, 0, 0); + worker.block_dirty(1, 0, 0); + assert_eq!(worker.awaited, Some(2)); + + reports.send(report(1, (0, 0))).unwrap(); + worker.poll(&mut store, &mut out); + assert_eq!(worker.awaited, Some(2)); + + reports.send(report(2, (0, 0))).unwrap(); + worker.poll(&mut store, &mut out); + assert_eq!(worker.awaited, None); + } +} diff --git a/pomme-client/src/world/light/world.rs b/pomme-client/src/world/light/world.rs index f175fa685..f1865dbe6 100644 --- a/pomme-client/src/world/light/world.rs +++ b/pomme-client/src/world/light/world.rs @@ -1,8 +1,10 @@ //! Block-state access for light propagation: vanilla `LightEngine.getState` -//! plus its 2-entry chunk cache. Positions in unloaded chunks read as bedrock -//! so propagation stops at loaded-area borders exactly like vanilla. +//! plus its 2-entry chunk cache, over the chunks the light thread was handed. +//! Positions in unloaded chunks read as bedrock so propagation stops at +//! loaded-area borders exactly like vanilla. use std::cell::RefCell; +use std::collections::HashMap; use std::sync::Arc; use azalea_block::BlockState; @@ -10,7 +12,7 @@ use parking_lot::RwLock; use super::storage::LightPos; use crate::world::block::BlockStateTable; -use crate::world::chunk::{Chunk, ChunkStore}; +use crate::world::chunk::Chunk; pub(crate) trait LightBlockGetter { fn block_state_table(&self) -> BlockStateTable; @@ -24,10 +26,11 @@ pub(crate) trait LightBlockGetter { type CacheEntry = ((i32, i32), Option>>); -/// [`ChunkStore`]-backed getter, constructed fresh per light-update run. -pub(crate) struct StoreWorld<'a> { +/// The light thread's view of the level: the loaded chunks, shared with the +/// game thread that edits them. +pub(crate) struct LightWorld { block_state_table: BlockStateTable, - store: &'a ChunkStore, + chunks: HashMap<(i32, i32), Arc>>, bedrock: BlockState, min_y: i32, cache: RefCell<[CacheEntry; 2]>, @@ -35,42 +38,71 @@ pub(crate) struct StoreWorld<'a> { const INVALID_CHUNK: (i32, i32) = (i32::MAX, i32::MAX); -impl<'a> StoreWorld<'a> { - pub fn new(store: &'a ChunkStore, block_state_table: BlockStateTable) -> Self { +impl LightWorld { + pub fn new(block_state_table: BlockStateTable, min_y: i32) -> Self { Self { block_state_table, - store, + chunks: HashMap::new(), bedrock: block_state_table .first_state_of("bedrock") .unwrap_or(BlockState::AIR), - min_y: store.min_y(), + min_y, cache: RefCell::new([(INVALID_CHUNK, None), (INVALID_CHUNK, None)]), } } - fn chunk(&self, cx: i32, cz: i32) -> Option>> { + pub fn min_y(&self) -> i32 { + self.min_y + } + + pub fn insert(&mut self, pos: (i32, i32), chunk: Arc>) { + self.chunks.insert(pos, chunk); + self.clear_cache(); + } + + pub fn remove(&mut self, pos: (i32, i32)) { + self.chunks.remove(&pos); + self.clear_cache(); + } + + pub fn chunk(&self, pos: (i32, i32)) -> Option>> { let mut cache = self.cache.borrow_mut(); - for (pos, chunk) in cache.iter() { - if *pos == (cx, cz) { + for (cached, chunk) in cache.iter() { + if *cached == pos { return chunk.clone(); } } - let chunk = self - .store - .get_chunk(&crate::position::ChunkPos::new(cx, cz)); + let chunk = self.chunks.get(&pos).cloned(); cache[1] = cache[0].clone(); - cache[0] = ((cx, cz), chunk.clone()); + cache[0] = (pos, chunk.clone()); chunk } + + /// Vanilla `LevelChunk.getSection(...).hasOnlyAir()`; unloaded reads as + /// empty. + pub fn section_is_empty(&self, pos: (i32, i32), section_y: i32) -> bool { + let Some(chunk) = self.chunk(pos) else { + return true; + }; + let index = section_y - (self.min_y >> 4); + let chunk = chunk.read(); + match usize::try_from(index) + .ok() + .and_then(|i| chunk.sections.get(i)) + { + Some(section) => section.block_count == 0, + None => true, + } + } } -impl LightBlockGetter for StoreWorld<'_> { +impl LightBlockGetter for LightWorld { fn block_state_table(&self) -> BlockStateTable { self.block_state_table } fn state(&self, pos: LightPos) -> BlockState { - let Some(chunk) = self.chunk(pos.x >> 4, pos.z >> 4) else { + let Some(chunk) = self.chunk((pos.x >> 4, pos.z >> 4)) else { return self.bedrock; }; let chunk = chunk.read(); diff --git a/tests/ingame/README.md b/tests/ingame/README.md index 84b7e209d..6a1ea8fb6 100644 --- a/tests/ingame/README.md +++ b/tests/ingame/README.md @@ -149,9 +149,11 @@ option = { main_hand = "left" } # sets client options, as the # options screen does; also # menu_background_blur, # brightness, render_distance - # (chunks) and shader_pack - # (a name in fixtures/shaderpacks, - # "" for none) + # (chunks), distant_terrain + # (chunks, 0 for off) and + # shader_pack (a name in + # fixtures/shaderpacks, "" for + # none) [[step]] deterministic = true # holds flicker, particles, # animated textures, clouds, view @@ -173,7 +175,9 @@ wait_ms = 1000 frame_times = { ms = 5000 } # times the frames for 5 s; the # report line gives the frame # rate, its 1% low, the render - # encode time and the GPU wait + # encode time, the GPU wait, the + # GPU's own time per pass, and + # the sections and triangles drawn [[step]] screenshot = "equipped" # target/ingame/