mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Store whole cube faces of near sections as one 24-byte record
Most solid quads of a section are whole faces of a block's cube on the block grid: their corners, UVs, light and colour follow from the block, the face and a few bytes. The mesher now lays such a quad out as one 24-byte record (block and face, UV rectangle, layer, tint, alpha, each corner's grey level and light) instead of four 24-byte vertices, and the rest stay as vertices that their record points to. The quads keep their order, so the side buckets, draws, occlusion culling and GPU culling count quads as before. A record is kept only when expanding it gives back the quad's four vertices bit for bit. The solid pipelines now read the arena page themselves through the flat draws' page groups, flat and per-section draws alike: a vertex index below 2^28 names a vertex, above it a corner of the record at that slot. They expand records to the same vertex words, so the shading after is unchanged; the colour is converted as the M1's vertex fetch converts Unorm8x4 (times 1/255), which keeps the old layout's picture exact through them. The visibility buffer reads records in its resolve; its IDs now hold a record bit and 6 bits of page, and frames with more than 64 pages draw without it. Translucent quads and far tiles keep their vertices. With a shader pack (or one of Horizon's own) the sections are meshed again as vertices, since its programs read the arena as such. HORIZON_PACKED_QUADS=0 turns the layout off. On the RD 48 capture, meshed again from its save, 30.4M of 34.4M solid quads get a record: near-section vertex data falls from 3,362 MB to 1,367 MB and the process's graphics memory from 5.4 GB to 3.4 GB, with the same GPU frame time. Per-section draws give the same image as before. Flat draws differ by 1/255 in 15 to 25 pixels of 3.7M: moving unchanged meshes to other places in the arena does the same (22 pixels), since the draws then group by page differently.
This commit is contained in:
1 parent
41cf95bc1e
commit
f2c8e2ab18
12 files changed
+847
-121
No files matched your search
@@ -29,7 +29,7 @@ use horizon_client_render::{
|
||||
graphics::LeafCullingMode,
|
||||
hud::HudBatch,
|
||||
lod::TileKey,
|
||||
mesher::{MeshScratch, MeshTables, Mesher, SectionMesh},
|
||||
mesher::{MeshScratch, MeshTables, Mesher, SectionMesh, packed},
|
||||
models,
|
||||
};
|
||||
use horizon_core::{
|
||||
@@ -286,10 +286,12 @@ fn replay(window: Arc<Window>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
let leaf_layers = leaf_layers(&render);
|
||||
let (mut sections, mut quads, mut leaf_quads) = (0, 0usize, 0usize);
|
||||
let mut packed_quads = 0usize;
|
||||
capture::read_meshes(&o.dir, "sections", |[x, y, z], captured| {
|
||||
if near(x, z) {
|
||||
let mesh = remeshed.remove(&[x, y, z]).unwrap_or(captured);
|
||||
quads += mesh.solid.len() / 4;
|
||||
packed_quads += packed::packed_quads(&mesh.packed, mesh.solid.len() / 4);
|
||||
leaf_quads += mesh
|
||||
.solid
|
||||
.iter()
|
||||
@@ -416,6 +418,8 @@ fn replay(window: Arc<Window>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
"sections": sections,
|
||||
"solid_quads": quads,
|
||||
"leaf_quads": leaf_quads,
|
||||
// Solid quads uploaded as one record each (`mesher::packed`).
|
||||
"packed_quads": packed_quads,
|
||||
"world": world_report,
|
||||
"tiles": tile_count,
|
||||
"load_ms": loaded_ms,
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::{
|
||||
fast_hash::FxHashMap,
|
||||
graphics::GraphicsApi,
|
||||
hud::{HudBatch, HudRenderer},
|
||||
mesher::{Part as Quads, SectionMesh, Sides, TerrainVertex, Vertex},
|
||||
mesher::{Part as Quads, SectionMesh, Sides, TerrainVertex, Vertex, packed},
|
||||
section_mask::SectionMask,
|
||||
sprites::SpriteAnimation,
|
||||
};
|
||||
@@ -131,6 +131,11 @@ fn translucent_split(batching: bool, flat_draws: bool, terrain: bool, shadow: bo
|
||||
batching && !(flat_draws && (terrain || shadow))
|
||||
}
|
||||
|
||||
/// The compact layout of solid quads, unless `HORIZON_PACKED_QUADS=0` (A/B checks).
|
||||
fn packed_wanted() -> bool {
|
||||
std::env::var("HORIZON_PACKED_QUADS").as_deref() != Ok("0")
|
||||
}
|
||||
|
||||
mod animate;
|
||||
mod arena;
|
||||
pub mod capture;
|
||||
@@ -396,6 +401,10 @@ struct SectionGpu {
|
||||
/// Unused without translucent quads.
|
||||
translucent_at: (u32, u64),
|
||||
solid: u32,
|
||||
/// Slots its solid quads take in `arena`: four vertices each, or in the compact layout
|
||||
/// (`packed`, `mesher::packed`) a record each and the vertices of those without one.
|
||||
solid_slots: u64,
|
||||
packed: bool,
|
||||
/// Where the solid quads' buckets by facing side end (`mesher::Sides`).
|
||||
sides: Sides,
|
||||
translucent: u32,
|
||||
@@ -447,12 +456,12 @@ impl SectionGpu {
|
||||
/// is one): (page, first vertex, vertices) of each.
|
||||
fn vertex_ranges(&self, split: bool) -> (Option<(u32, u64, u64)>, Option<(u32, u64, u64)>) {
|
||||
if !split {
|
||||
let vertices = u64::from(self.solid + self.translucent) * 4;
|
||||
let vertices = self.solid_slots + u64::from(self.translucent) * 4;
|
||||
return (Some((self.page, self.start, vertices)), None);
|
||||
}
|
||||
let (page, start) = self.translucent_at;
|
||||
(
|
||||
(self.solid > 0).then(|| (self.page, self.start, u64::from(self.solid) * 4)),
|
||||
(self.solid > 0).then(|| (self.page, self.start, self.solid_slots)),
|
||||
(self.translucent > 0).then(|| (page, start, u64::from(self.translucent) * 4)),
|
||||
)
|
||||
}
|
||||
@@ -462,6 +471,16 @@ impl SectionGpu {
|
||||
fn gpu_recorded(&self, key: SectionKey, sections: usize) -> bool {
|
||||
self.solid > 0 && (0..sections as i32).contains(&key.1)
|
||||
}
|
||||
/// The base vertex of its solid draws: quad `q`'s corners are `base + 4 * q` on (the
|
||||
/// quad indices). In the compact layout they name its record's slot past
|
||||
/// `PACKED_BASE` (terrain.wgsl's `section_corner`).
|
||||
fn solid_base(&self) -> u32 {
|
||||
if self.packed {
|
||||
packed::PACKED_BASE + 4 * self.start as u32
|
||||
} else {
|
||||
self.start as u32
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Which of a mesh's ranges `move_off_page` moves: what it holds in the section
|
||||
/// arena (`split`: its solid quads alone, `SectionGpu::vertex_ranges`), its
|
||||
@@ -490,7 +509,7 @@ impl Part {
|
||||
(s.page, s.start) = to;
|
||||
if !split {
|
||||
// Its translucent quads came along, after the solid ones.
|
||||
s.translucent_at = (to.0, to.1 + u64::from(s.solid) * 4);
|
||||
s.translucent_at = (to.0, to.1 + s.solid_slots);
|
||||
}
|
||||
}
|
||||
Self::Translucent => s.translucent_at = to,
|
||||
@@ -547,7 +566,7 @@ fn move_off_page<'a>(
|
||||
if let (Part::Section { .. }, Some(records), Some(id)) =
|
||||
(part, records.as_deref_mut(), s.gpu_id)
|
||||
{
|
||||
records.relocate(id, to.0, to.1);
|
||||
records.relocate(id, to.0, u64::from(s.solid_base()));
|
||||
}
|
||||
*budget = budget.saturating_sub(bytes);
|
||||
moved = true;
|
||||
@@ -858,6 +877,9 @@ pub struct Renderer {
|
||||
translucent_mark: wgpu::RenderPipeline,
|
||||
/// Flat section draws (`flat`), where multi-draw is emulated.
|
||||
flat: Option<flat::Flat>,
|
||||
/// Sections upload their solid quads in the compact layout (`mesher::packed`): the solid
|
||||
/// passes read the arena themselves (`flat`) and no shader pack reads it as vertices.
|
||||
packed_quads: bool,
|
||||
/// The visibility buffer for flat frames' solid terrain (`visbuffer`,
|
||||
/// `HORIZON_VISBUFFER=1`).
|
||||
visbuf: Option<visbuffer::Visbuffer>,
|
||||
@@ -2003,7 +2025,7 @@ impl Renderer {
|
||||
.sections
|
||||
.values()
|
||||
.chain(self.lods.values().map(|tile| &tile.mesh))
|
||||
.map(|s| u64::from(s.solid + s.translucent) * 4)
|
||||
.map(|s| s.solid_slots + u64::from(s.translucent) * 4)
|
||||
.sum();
|
||||
let translucent = self.translucent_arena.as_ref();
|
||||
let arenas = self.arena.bytes() + translucent.map_or(0, arena::Arena::bytes);
|
||||
@@ -2015,14 +2037,20 @@ impl Renderer {
|
||||
/// draws' buffers and the visibility buffer's.
|
||||
pub fn gpu_census(&self) -> Vec<(&'static str, u64)> {
|
||||
let stride = std::mem::size_of::<TerrainVertex>() as u64;
|
||||
let used = |s: &SectionGpu| u64::from(s.solid + s.translucent) * 4 * stride;
|
||||
let used = |s: &SectionGpu| (s.solid_slots + u64::from(s.translucent) * 4) * stride;
|
||||
let textures = |t: &[wgpu::Texture]| t.iter().map(texture_bytes).sum::<u64>();
|
||||
let translucent = self.translucent_arena.as_ref();
|
||||
vec![
|
||||
("section_arena_pages", self.arena.bytes()),
|
||||
("translucent_arena_pages", translucent.map_or(0, arena::Arena::bytes)),
|
||||
(
|
||||
"translucent_arena_pages",
|
||||
translucent.map_or(0, arena::Arena::bytes),
|
||||
),
|
||||
("sorted_index_pages", self.sorted_indices.bytes()),
|
||||
("near_section_vertices", self.sections.values().map(used).sum()),
|
||||
(
|
||||
"near_section_vertices",
|
||||
self.sections.values().map(used).sum(),
|
||||
),
|
||||
(
|
||||
"far_tile_vertices",
|
||||
self.lods.values().map(|tile| used(&tile.mesh)).sum(),
|
||||
@@ -2033,7 +2061,10 @@ impl Renderer {
|
||||
"pbr_maps",
|
||||
self.pbr_maps.as_ref().map_or(0, |maps| textures(maps)),
|
||||
),
|
||||
("flat_draws", self.flat.as_ref().map_or(0, flat::Flat::bytes)),
|
||||
(
|
||||
"flat_draws",
|
||||
self.flat.as_ref().map_or(0, flat::Flat::bytes),
|
||||
),
|
||||
(
|
||||
"visibility_buffer",
|
||||
self.visbuf.as_ref().map_or(0, visbuffer::Visbuffer::bytes),
|
||||
@@ -3938,13 +3969,69 @@ impl Renderer {
|
||||
&bindings,
|
||||
arena.page_vertices(),
|
||||
min_sections,
|
||||
|layout, label, vertex, fragment, blend, cull| {
|
||||
|layout, label, vertex, fragment, blend, cull, pulled| {
|
||||
let buffers: &[Option<wgpu::VertexBufferLayout<'_>>] =
|
||||
if pulled { &[] } else { &buffers };
|
||||
pipeline_in(
|
||||
layout, label, vertex, fragment, &buffers, tri, cull, true, true, blend,
|
||||
layout, label, vertex, fragment, buffers, tri, cull, true, true, blend,
|
||||
)
|
||||
},
|
||||
)
|
||||
});
|
||||
// With flat draws' page groups the per-section solid draws read their vertices from
|
||||
// the page too (`Flat::page_group`), in either layout of the solid quads
|
||||
// (`mesher::packed`): only the origins stay a vertex buffer, at slot 1.
|
||||
let (solid, solid_opaque, solid_cutout, solid_mirror) = match &flat {
|
||||
Some(f) => {
|
||||
let pulled_layout = kept.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("Horizon terrain layout (pulled)"),
|
||||
bind_group_layouts: &[Some(&bindings), Some(&f.draw_layout)],
|
||||
..Default::default()
|
||||
});
|
||||
let origins = [None, terrain_buffers[1].clone()];
|
||||
let pulled = |label: &str, vertex: &str, fragment: &str, cull| {
|
||||
pipeline_in(
|
||||
&pulled_layout,
|
||||
label,
|
||||
vertex,
|
||||
fragment,
|
||||
&origins,
|
||||
tri,
|
||||
Some(cull),
|
||||
true,
|
||||
true,
|
||||
None,
|
||||
)
|
||||
};
|
||||
let back = wgpu::Face::Back;
|
||||
(
|
||||
pulled("Terrain solid", "vs_section", "fs_solid", back),
|
||||
pulled(
|
||||
"Terrain solid opaque",
|
||||
"vs_section_opaque",
|
||||
"fs_opaque",
|
||||
back,
|
||||
),
|
||||
pulled(
|
||||
"Terrain solid cutout",
|
||||
"vs_section_cutout",
|
||||
"fs_solid",
|
||||
back,
|
||||
),
|
||||
pulled(
|
||||
"Terrain solid (mirrored)",
|
||||
"vs_section",
|
||||
"fs_solid_mirror",
|
||||
wgpu::Face::Front,
|
||||
),
|
||||
)
|
||||
}
|
||||
None => (solid, solid_opaque, solid_cutout, solid_mirror),
|
||||
};
|
||||
// The solid quads in the compact layout, while no shader pack reads the arena
|
||||
// (`follow_packed_layout`).
|
||||
let packed_quads = flat.is_some() && !shader_pack && packed_wanted();
|
||||
packed::pack_quads(packed_quads);
|
||||
let visbuf = flat
|
||||
.as_ref()
|
||||
.filter(|_| visbuffer::wanted(&device))
|
||||
@@ -4094,6 +4181,7 @@ impl Renderer {
|
||||
translucent,
|
||||
translucent_mark,
|
||||
flat_on: flat.is_some(),
|
||||
packed_quads,
|
||||
cull_on: cull.is_some(),
|
||||
sides_on: true,
|
||||
gpu_culling: None,
|
||||
@@ -4600,7 +4688,8 @@ impl Renderer {
|
||||
let sections = self.layout.sections();
|
||||
for (key, s) in self.sections.iter_mut() {
|
||||
if s.gpu_recorded(key, sections) {
|
||||
s.gpu_id = Some(gpu.records.insert(key, s.page, s.start, s.solid));
|
||||
let base = u64::from(s.solid_base());
|
||||
s.gpu_id = Some(gpu.records.insert(key, s.page, base, s.solid));
|
||||
}
|
||||
}
|
||||
self.gpu_culling = Some(gpu);
|
||||
@@ -4627,7 +4716,8 @@ impl Renderer {
|
||||
let recorded = s.gpu_recorded(key, sections);
|
||||
assert_eq!(s.gpu_id.is_some(), recorded, "{key:?}: a record");
|
||||
if let Some(id) = s.gpu_id {
|
||||
let record = gpu_culling::Record::new(key, s.page, s.start, s.solid);
|
||||
let base = u64::from(s.solid_base());
|
||||
let record = gpu_culling::Record::new(key, s.page, base, s.solid);
|
||||
assert_eq!(records.at(id), Some(&record), "{key:?}: its record {id}");
|
||||
pages[s.page as usize] += 1;
|
||||
live += 1;
|
||||
@@ -4765,8 +4855,8 @@ impl Renderer {
|
||||
if let Some(gpu) = &mut self.gpu_culling
|
||||
&& section.gpu_recorded(key, self.layout.sections())
|
||||
{
|
||||
let (page, start, quads) = (section.page, section.start, section.solid);
|
||||
section.gpu_id = Some(gpu.records.insert(key, page, start, quads));
|
||||
let (page, base, quads) = (section.page, section.solid_base(), section.solid);
|
||||
section.gpu_id = Some(gpu.records.insert(key, page, u64::from(base), quads));
|
||||
}
|
||||
self.sections.insert_with_bounds(key, section, mesh.bounds);
|
||||
}
|
||||
@@ -4790,13 +4880,20 @@ impl Renderer {
|
||||
});
|
||||
// The solid quads, and the translucent ones after them unless they have
|
||||
// an arena of their own. With video and system memory both full, the
|
||||
// section is not drawn.
|
||||
// section is not drawn. The solid ones in the compact layout where the
|
||||
// mesher made it and the passes read it (`packed_quads`).
|
||||
let packed = self.packed_quads && !mesh.packed.is_empty() && solid > 0;
|
||||
let solid_slots = if packed {
|
||||
mesh.packed.len() as u64
|
||||
} else {
|
||||
mesh.solid.len() as u64
|
||||
};
|
||||
let split = self.translucent_arena.is_some();
|
||||
let main = if split {
|
||||
mesh.solid.len()
|
||||
solid_slots
|
||||
} else {
|
||||
mesh.solid.len() + mesh.translucent.len()
|
||||
} as u64;
|
||||
solid_slots + mesh.translucent.len() as u64
|
||||
};
|
||||
let (page, start) = if main > 0 {
|
||||
self.arena.alloc(&self.device, encoder, main)?
|
||||
} else {
|
||||
@@ -4818,7 +4915,7 @@ impl Renderer {
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (page, start + u64::from(solid) * 4),
|
||||
_ => (page, start + solid_slots),
|
||||
};
|
||||
let sorted_indices = if translucent > 0 {
|
||||
let indices =
|
||||
@@ -4855,12 +4952,20 @@ impl Renderer {
|
||||
});
|
||||
let stride = std::mem::size_of::<TerrainVertex>() as u64;
|
||||
let translucent_arena = self.translucent_arena.as_ref().unwrap_or(&self.arena);
|
||||
for (part, arena, (part_page, first)) in [
|
||||
(&mesh.solid, &self.arena, (page, start)),
|
||||
(&mesh.translucent, translucent_arena, translucent_at),
|
||||
let solid_bytes: &[u8] = if packed {
|
||||
bytemuck::cast_slice(&mesh.packed)
|
||||
} else {
|
||||
bytemuck::cast_slice(&mesh.solid)
|
||||
};
|
||||
for (bytes, arena, (part_page, first)) in [
|
||||
(solid_bytes, &self.arena, (page, start)),
|
||||
(
|
||||
bytemuck::cast_slice(&mesh.translucent),
|
||||
translucent_arena,
|
||||
translucent_at,
|
||||
),
|
||||
] {
|
||||
if !part.is_empty() {
|
||||
let bytes: &[u8] = bytemuck::cast_slice(part);
|
||||
if !bytes.is_empty() {
|
||||
self.uploaded_bytes += bytes.len() as u64;
|
||||
self.section_copy_bytes += bytes.len() as u64;
|
||||
self.section_uploads.buffer(
|
||||
@@ -4883,6 +4988,8 @@ impl Renderer {
|
||||
start,
|
||||
translucent_at,
|
||||
solid,
|
||||
solid_slots,
|
||||
packed,
|
||||
sides: if mesh.sides.quads() == solid {
|
||||
mesh.sides
|
||||
} else {
|
||||
@@ -4925,6 +5032,29 @@ impl Renderer {
|
||||
base: u32,
|
||||
first: u32,
|
||||
count: u32,
|
||||
) {
|
||||
self.draw_sections_from(pass, indirect, base, first, count, false);
|
||||
}
|
||||
/// `draw_sections` through the solid pipelines that read the arena page themselves
|
||||
/// (`vs_section`): each page's group is bound as their group 1.
|
||||
fn draw_solid_sections(
|
||||
&self,
|
||||
pass: &mut wgpu::RenderPass<'_>,
|
||||
indirect: &wgpu::Buffer,
|
||||
base: u32,
|
||||
first: u32,
|
||||
count: u32,
|
||||
) {
|
||||
self.draw_sections_from(pass, indirect, base, first, count, self.flat.is_some());
|
||||
}
|
||||
fn draw_sections_from(
|
||||
&self,
|
||||
pass: &mut wgpu::RenderPass<'_>,
|
||||
indirect: &wgpu::Buffer,
|
||||
base: u32,
|
||||
first: u32,
|
||||
count: u32,
|
||||
pulled: bool,
|
||||
) {
|
||||
let (draws, pages) = (&self.draws[..], &self.draw_pages[..]);
|
||||
let (mut i, end) = (first as usize, (first + count) as usize);
|
||||
@@ -4945,6 +5075,12 @@ impl Renderer {
|
||||
),
|
||||
};
|
||||
pass.set_vertex_buffer(0, vertices);
|
||||
if pulled
|
||||
&& index_page.is_none()
|
||||
&& let Some(group) = self.flat.as_ref().and_then(|f| f.page_group(page))
|
||||
{
|
||||
pass.set_bind_group(1, group, &[]);
|
||||
}
|
||||
pass.set_index_buffer(indices, wgpu::IndexFormat::Uint32);
|
||||
if self.multi_draw {
|
||||
let offset = (i - base as usize) as u64 * 20;
|
||||
@@ -5289,6 +5425,23 @@ impl Renderer {
|
||||
flat.set_translucent_page_vertices(arena.page_vertices());
|
||||
}
|
||||
}
|
||||
/// Uploads solid quads in the compact layout (`packed_quads`) while no shader pack runs:
|
||||
/// a pack's terrain and shadow programs, and Horizon's own packs' stages, read the
|
||||
/// arena's vertices as `TerrainVertex`es. Leaving it drops every section and far tile
|
||||
/// for the game to mesh again, as `follow_translucent_layout` does.
|
||||
fn follow_packed_layout(&mut self) {
|
||||
let pack = self.pack_post.as_ref().and_then(|p| p.1.as_ref()).is_some();
|
||||
let want = self.flat.is_some() && !pack && self.native.is_none() && packed_wanted();
|
||||
if want == self.packed_quads {
|
||||
return;
|
||||
}
|
||||
self.packed_quads = want;
|
||||
packed::pack_quads(want);
|
||||
if !want && self.sections.values().any(|s| s.packed) {
|
||||
self.sections_dropped = true;
|
||||
self.clear_sections(self.layout);
|
||||
}
|
||||
}
|
||||
/// Whether every section and far tile was dropped since the last call, to be
|
||||
/// laid out again (`follow_translucent_layout`): the game meshes them again,
|
||||
/// as for a new renderer.
|
||||
@@ -6263,6 +6416,7 @@ impl Renderer {
|
||||
// so the next world's sections, which the level loading screen uploads, stay.
|
||||
if !frame.title {
|
||||
self.follow_translucent_layout();
|
||||
self.follow_packed_layout();
|
||||
}
|
||||
let released = self.arena.trim();
|
||||
self.graveyard.add(released);
|
||||
@@ -6996,7 +7150,7 @@ impl Renderer {
|
||||
let start = draws.len();
|
||||
facing_runs_from(&s.sides, eye, mirror_low, v.origin, part, |first, quads| {
|
||||
indices += quads * 6;
|
||||
let base = s.start as u32 + first * 4;
|
||||
let base = s.solid_base() + first * 4;
|
||||
draws.push([quads * 6, instances, 0, base, i as u32]);
|
||||
pages.push((s.page, None));
|
||||
});
|
||||
@@ -7054,7 +7208,7 @@ impl Renderer {
|
||||
let start = draws.len();
|
||||
facing_runs_from(&s.sides, eye, mirror_low, v.origin, part, |first, quads| {
|
||||
indices += quads * 6;
|
||||
let base = s.start as u32 + first * 4;
|
||||
let base = s.solid_base() + first * 4;
|
||||
draws.push([quads * 6, instances, 0, base, index as u32]);
|
||||
pages.push((s.page, None));
|
||||
});
|
||||
@@ -7279,7 +7433,7 @@ impl Renderer {
|
||||
});
|
||||
let toward = (!unculled).then_some(toward);
|
||||
facing_runs(&s.sides, toward, v.origin, |first, quads| {
|
||||
let start = s.start as u32 + first * 4;
|
||||
let start = s.solid_base() + first * 4;
|
||||
self.draws.push([quads * 6, 1, 0, start, instance]);
|
||||
self.draw_pages.push((s.page, None));
|
||||
});
|
||||
@@ -7870,6 +8024,13 @@ impl Renderer {
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
// Each page's group binds the page for the solid draws, flat or not.
|
||||
if let Some(flat) = &mut self.flat {
|
||||
let arena = &self.arena;
|
||||
flat.bind_pages(&self.device, |page| {
|
||||
arena.has_page(page).then(|| arena.buffer(page))
|
||||
});
|
||||
}
|
||||
if flat_frame {
|
||||
self.section_draw_calls = self.flat.as_ref().map_or(0, |f| f.shadow_calls[0]);
|
||||
} else if self.multi_draw && !self.draws.is_empty() {
|
||||
@@ -8299,7 +8460,7 @@ impl Renderer {
|
||||
pass.set_pipeline(&self.solid_mirror);
|
||||
pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32);
|
||||
pass.set_vertex_buffer(1, self.instances.slice(..));
|
||||
self.draw_sections(&mut pass, &self.indirect, 0, 0, solid_draws);
|
||||
self.draw_solid_sections(&mut pass, &self.indirect, 0, 0, solid_draws);
|
||||
}
|
||||
drop(pass);
|
||||
reflection = Some(mirror.color.clone());
|
||||
@@ -8374,6 +8535,10 @@ impl Renderer {
|
||||
&& material.is_none()
|
||||
&& surface.is_none()
|
||||
&& shadow_view.is_none()
|
||||
&& self
|
||||
.arena
|
||||
.page_buffers()
|
||||
.all(|(page, _)| page < visbuffer::MAX_PAGES)
|
||||
&& self.visbuf.is_some();
|
||||
if vis_frame && let (Some(vis), Some(flat)) = (&mut self.visbuf, &self.flat) {
|
||||
let depth = world_depth.texture();
|
||||
@@ -8626,6 +8791,9 @@ impl Renderer {
|
||||
],
|
||||
_ => &[(solid_pipeline, 0..phase1_draws, phase1_draws..solid_draws)],
|
||||
};
|
||||
// The vanilla solid pipelines read the page themselves; a native
|
||||
// material's take it as vertices.
|
||||
let pulled = material.is_none() && self.flat.is_some();
|
||||
pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32);
|
||||
pass.set_vertex_buffer(1, self.instances.slice(..));
|
||||
match cull {
|
||||
@@ -8636,12 +8804,13 @@ impl Renderer {
|
||||
// both targets (it reads the test's results).
|
||||
for (pipeline, draws, _) in passes {
|
||||
pass.set_pipeline(pipeline);
|
||||
self.draw_sections(
|
||||
self.draw_sections_from(
|
||||
&mut pass,
|
||||
&self.indirect,
|
||||
0,
|
||||
draws.start,
|
||||
draws.len() as u32,
|
||||
pulled,
|
||||
);
|
||||
}
|
||||
if surface.is_some() {
|
||||
@@ -8671,12 +8840,13 @@ impl Renderer {
|
||||
pass.set_vertex_buffer(1, self.instances.slice(..));
|
||||
for (pipeline, _, draws) in passes {
|
||||
pass.set_pipeline(pipeline);
|
||||
self.draw_sections(
|
||||
self.draw_sections_from(
|
||||
&mut pass,
|
||||
&cull.phase2,
|
||||
phase1_draws,
|
||||
draws.start,
|
||||
draws.len() as u32,
|
||||
pulled,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8686,15 +8856,20 @@ impl Renderer {
|
||||
pass.set_pipeline(pipeline);
|
||||
// GPU culling's draws first, all within the render distance.
|
||||
if gpu_frame && let Some(gpu) = &self.gpu_culling {
|
||||
gpu.draw(&mut pass, &self.arena);
|
||||
gpu.draw(&mut pass, &self.arena, |page| {
|
||||
pulled
|
||||
.then(|| self.flat.as_ref()?.page_group(page))
|
||||
.flatten()
|
||||
});
|
||||
pass.set_vertex_buffer(1, self.instances.slice(..));
|
||||
}
|
||||
self.draw_sections(
|
||||
self.draw_sections_from(
|
||||
&mut pass,
|
||||
&self.indirect,
|
||||
0,
|
||||
draws.start,
|
||||
draws.len() as u32,
|
||||
pulled,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -877,6 +877,8 @@ mod tests {
|
||||
start,
|
||||
translucent_at,
|
||||
solid,
|
||||
solid_slots: u64::from(solid) * 4,
|
||||
packed: false,
|
||||
sides: crate::mesher::Sides::unsorted(solid),
|
||||
translucent,
|
||||
sort: super::super::translucency::SortState::new(Vec::new()),
|
||||
@@ -1014,7 +1016,7 @@ mod tests {
|
||||
label: None,
|
||||
entries: &[],
|
||||
});
|
||||
let pipeline = |layout: &wgpu::PipelineLayout, label: &str, _: &str, _: &str, _, _| {
|
||||
let pipeline = |layout: &wgpu::PipelineLayout, label: &str, _: &str, _: &str, _, _, _| {
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(label),
|
||||
layout: Some(layout),
|
||||
@@ -1071,6 +1073,8 @@ mod tests {
|
||||
start: 0,
|
||||
translucent_at: (0, at),
|
||||
solid: 64,
|
||||
solid_slots: 64 * 4,
|
||||
packed: false,
|
||||
sides: crate::mesher::Sides::unsorted(64),
|
||||
translucent: 128,
|
||||
sort: super::super::translucency::SortState::new(Vec::new()),
|
||||
@@ -1295,6 +1299,8 @@ mod tests {
|
||||
start: 0,
|
||||
translucent_at: if indices { (7, 7) } else { at },
|
||||
solid,
|
||||
solid_slots: u64::from(solid) * 4,
|
||||
packed: false,
|
||||
sides: crate::mesher::Sides::unsorted(solid),
|
||||
translucent,
|
||||
sort: super::super::translucency::SortState::new(Vec::new()),
|
||||
|
||||
@@ -66,6 +66,8 @@ fn copy(mesh: &SectionMesh) -> SectionMesh {
|
||||
solid: mesh.solid.clone(),
|
||||
sides: mesh.sides,
|
||||
translucent: mesh.translucent.clone(),
|
||||
packed: mesh.packed.clone(),
|
||||
solid_tints: Vec::new(),
|
||||
visibility: mesh.visibility,
|
||||
block_entities: mesh.block_entities.clone(),
|
||||
lights: mesh.lights.clone(),
|
||||
@@ -320,6 +322,7 @@ pub fn read_meshes(
|
||||
.bounds
|
||||
.map(|[min, max]| [Vec3::from_array(min), Vec3::from_array(max)]),
|
||||
keep: record.keep,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,10 +54,15 @@ const GROUP_LIMIT: u32 = 65535;
|
||||
/// below it. A call names its key: its vertices come from that arena's page.
|
||||
pub const TRANSLUCENT_KEYS: u32 = 1 << 10;
|
||||
|
||||
/// terrain.wgsl's `arena_page`: the page the solid draws read their vertices from.
|
||||
pub const ARENA_BINDING: u32 = 19;
|
||||
|
||||
struct PageTable {
|
||||
/// Section id of every block of the page.
|
||||
blocks: wgpu::Buffer,
|
||||
group: Option<wgpu::BindGroup>,
|
||||
/// The section arena's page `group` binds at `ARENA_BINDING` (`bind_pages`).
|
||||
arena: Option<wgpu::Buffer>,
|
||||
}
|
||||
|
||||
/// Writes the fragment's alpha over the frame's and leaves its colour.
|
||||
@@ -350,8 +355,9 @@ fn plan(
|
||||
}
|
||||
|
||||
impl Flat {
|
||||
/// `pipeline(layout, label, vertex entry, fragment entry, blend, cull)` builds a
|
||||
/// terrain pipeline as the per-section ones, without the origin buffer.
|
||||
/// `pipeline(layout, label, vertex entry, fragment entry, blend, cull, pulled)` builds a
|
||||
/// terrain pipeline as the per-section ones, without the origin buffer, and without
|
||||
/// vertex buffers when `pulled` (its vertex shader reads the page, `ARENA_BINDING`).
|
||||
pub fn new(
|
||||
device: &wgpu::Device,
|
||||
terrain_layout: &wgpu::BindGroupLayout,
|
||||
@@ -364,13 +370,18 @@ impl Flat {
|
||||
&str,
|
||||
Option<wgpu::BlendState>,
|
||||
Option<wgpu::Face>,
|
||||
bool,
|
||||
) -> wgpu::RenderPipeline,
|
||||
) -> Self {
|
||||
let vertex = wgpu::ShaderStages::VERTEX;
|
||||
let compute = wgpu::ShaderStages::COMPUTE;
|
||||
let draw_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("Flat section draws"),
|
||||
entries: &[storage(0, vertex, true), storage(1, vertex, true)],
|
||||
entries: &[
|
||||
storage(0, vertex, true),
|
||||
storage(1, vertex, true),
|
||||
storage(ARENA_BINDING, vertex, true),
|
||||
],
|
||||
});
|
||||
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("Flat terrain layout"),
|
||||
@@ -419,38 +430,44 @@ impl Flat {
|
||||
let storage_usage = wgpu::BufferUsages::STORAGE;
|
||||
let back = Some(wgpu::Face::Back);
|
||||
Self {
|
||||
// The solid draws read either layout of the solid quads (`mesher::packed`) from
|
||||
// the page; translucent ones keep their vertex buffer.
|
||||
solid: pipeline(
|
||||
&layout,
|
||||
"Terrain solid (flat)",
|
||||
"vs_terrain_flat",
|
||||
"vs_section_flat",
|
||||
"fs_solid",
|
||||
None,
|
||||
back,
|
||||
true,
|
||||
),
|
||||
solid_opaque: pipeline(
|
||||
&layout,
|
||||
"Terrain solid opaque (flat)",
|
||||
"vs_terrain_flat_opaque",
|
||||
"vs_section_flat_opaque",
|
||||
"fs_opaque",
|
||||
None,
|
||||
back,
|
||||
true,
|
||||
),
|
||||
solid_cutout: pipeline(
|
||||
&layout,
|
||||
"Terrain solid cutout (flat)",
|
||||
"vs_terrain_flat_cutout",
|
||||
"vs_section_flat_cutout",
|
||||
"fs_solid",
|
||||
None,
|
||||
back,
|
||||
true,
|
||||
),
|
||||
// The mirror flips the winding: its solid terrain culls the front.
|
||||
solid_mirror: pipeline(
|
||||
&layout,
|
||||
"Terrain solid mirrored (flat)",
|
||||
"vs_terrain_flat",
|
||||
"vs_section_flat",
|
||||
"fs_solid_mirror",
|
||||
None,
|
||||
Some(wgpu::Face::Front),
|
||||
true,
|
||||
),
|
||||
translucent: pipeline(
|
||||
&layout,
|
||||
@@ -459,6 +476,7 @@ impl Flat {
|
||||
"fs_translucent",
|
||||
Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
back,
|
||||
false,
|
||||
),
|
||||
translucent_mark: pipeline(
|
||||
&layout,
|
||||
@@ -467,6 +485,7 @@ impl Flat {
|
||||
"fs_translucent_mark",
|
||||
Some(ALPHA_ONLY),
|
||||
back,
|
||||
false,
|
||||
),
|
||||
flatten,
|
||||
compute_layout,
|
||||
@@ -563,6 +582,7 @@ impl Flat {
|
||||
wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
|
||||
),
|
||||
group: None,
|
||||
arena: None,
|
||||
});
|
||||
let first = start / GRAIN;
|
||||
let ids = vec![id; vertices.div_ceil(GRAIN) as usize];
|
||||
@@ -825,29 +845,53 @@ impl Flat {
|
||||
pass.dispatch_workgroups((*count).min(GROUP_LIMIT), count.div_ceil(GROUP_LIMIT), 1);
|
||||
}
|
||||
drop(pass);
|
||||
for page in self.calls.iter().map(|c| c.page) {
|
||||
let table = self.pages[page as usize]
|
||||
.as_mut()
|
||||
.expect("a drawn page has its block table");
|
||||
if table.group.is_none() {
|
||||
table.group = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Flat section draws"),
|
||||
layout: &self.draw_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: table.blocks.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: self.origins.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
}));
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
/// Gives every page table its bind group (`bind_page`): its blocks, the origins and, for
|
||||
/// a page of the section arena, that page (`arena`, by page) for the solid draws to read.
|
||||
/// Before each frame's draws, flat or not: the per-section solid draws bind the groups too
|
||||
/// (`page_group`), and a page that grew is another buffer.
|
||||
pub fn bind_pages<'a>(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
arena: impl Fn(u32) -> Option<&'a wgpu::Buffer>,
|
||||
) {
|
||||
for (key, slot) in self.pages.iter_mut().enumerate() {
|
||||
let Some(table) = slot else {
|
||||
continue;
|
||||
};
|
||||
let page = (key < TRANSLUCENT_KEYS as usize)
|
||||
.then(|| arena(key as u32))
|
||||
.flatten();
|
||||
if table.group.is_some() && table.arena.as_ref() == page {
|
||||
continue;
|
||||
}
|
||||
table.arena = page.cloned();
|
||||
table.group = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Flat section draws"),
|
||||
layout: &self.draw_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: table.blocks.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: self.origins.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: ARENA_BINDING,
|
||||
resource: page.unwrap_or(&self.empty).as_entire_binding(),
|
||||
},
|
||||
],
|
||||
}));
|
||||
}
|
||||
}
|
||||
/// The bind group of the section arena's page `page` (`bind_pages`), which the solid
|
||||
/// draws read their vertices through.
|
||||
pub fn page_group(&self, page: u32) -> Option<&wgpu::BindGroup> {
|
||||
self.pages.get(page as usize)?.as_ref()?.group.as_ref()
|
||||
}
|
||||
/// After occlusion culling's test: copies the phase 2 draws it saw.
|
||||
pub fn flatten_phase2(
|
||||
&self,
|
||||
|
||||
@@ -725,14 +725,23 @@ impl GpuCulling {
|
||||
}
|
||||
|
||||
/// The chosen sections' solid draws, a count draw per page, with the quad
|
||||
/// indices bound. Leaves the origins at vertex slot 1.
|
||||
pub fn draw(&self, pass: &mut wgpu::RenderPass<'_>, arena: &Arena) {
|
||||
/// indices bound. Leaves the origins at vertex slot 1. `group`: the page's group 1 for
|
||||
/// pipelines that read the page themselves (`Flat::page_group`), else none.
|
||||
pub fn draw<'a>(
|
||||
&self,
|
||||
pass: &mut wgpu::RenderPass<'_>,
|
||||
arena: &Arena,
|
||||
group: impl Fn(u32) -> Option<&'a wgpu::BindGroup>,
|
||||
) {
|
||||
if self.pages.is_empty() {
|
||||
return;
|
||||
}
|
||||
pass.set_vertex_buffer(1, self.origins.buffer.slice(..));
|
||||
for &(page, first, size) in &self.pages {
|
||||
pass.set_vertex_buffer(0, arena.buffer(page).slice(..));
|
||||
if let Some(group) = group(page) {
|
||||
pass.set_bind_group(1, group, &[]);
|
||||
}
|
||||
pass.multi_draw_indexed_indirect_count(
|
||||
&self.draws.buffer,
|
||||
u64::from(first) * 20,
|
||||
@@ -1390,7 +1399,7 @@ struct Quad {
|
||||
});
|
||||
pass.set_pipeline(&pipeline);
|
||||
pass.set_index_buffer(indices.slice(..), wgpu::IndexFormat::Uint32);
|
||||
gpu.draw(&mut pass, &arena);
|
||||
gpu.draw(&mut pass, &arena, |_| None);
|
||||
drop(pass);
|
||||
encoder.copy_texture_to_buffer(
|
||||
target.as_image_copy(),
|
||||
|
||||
@@ -21,6 +21,8 @@ use super::pack_timing::PassTiming;
|
||||
|
||||
/// Arena pages one resolve draw reads (`visbuffer.wgsl`'s `vis_page_0`...).
|
||||
pub const PAGE_SLOTS: u32 = 16;
|
||||
/// Arena pages an ID names (`visbuffer.wgsl`'s `vis_id`): frames with more draw without it.
|
||||
pub const MAX_PAGES: u32 = 64;
|
||||
/// The resolve binds that many pages, the block tables and the origins by section id.
|
||||
const STORAGE_BUFFERS: u32 = PAGE_SLOTS + 2;
|
||||
const IDS: wgpu::TextureFormat = wgpu::TextureFormat::R32Uint;
|
||||
@@ -110,11 +112,6 @@ impl Visbuffer {
|
||||
bind_group_layouts: &[Some(terrain_layout), Some(&page_layout)],
|
||||
..Default::default()
|
||||
});
|
||||
let vertices = [Some(wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<super::TerrainVertex>() as u64,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &super::TERRAIN_ATTRIBUTES,
|
||||
})];
|
||||
let ids = |label: &'static str, vertex: &'static str, fragment: &'static str| {
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(label),
|
||||
@@ -123,7 +120,8 @@ impl Visbuffer {
|
||||
module: &module,
|
||||
entry_point: Some(vertex),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &vertices,
|
||||
// `section_corner` reads the arena page (`flat::ARENA_BINDING`).
|
||||
buffers: &[],
|
||||
},
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
@@ -313,7 +311,10 @@ impl Visbuffer {
|
||||
/// Bytes of its block tables and IDs.
|
||||
pub fn bytes(&self) -> u64 {
|
||||
let blocks = self.blocks.as_ref().map_or(0, |(b, _)| b.size());
|
||||
let ids = self.ids.as_ref().map_or(0, |(_, [w, h])| u64::from(*w) * u64::from(*h) * 4);
|
||||
let ids = self
|
||||
.ids
|
||||
.as_ref()
|
||||
.map_or(0, |(_, [w, h])| u64::from(*w) * u64::from(*h) * 4);
|
||||
blocks + ids
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
mod fluid;
|
||||
mod leaf_culling;
|
||||
mod lighting;
|
||||
pub mod packed;
|
||||
mod sides;
|
||||
use crate::connected::{Pick, TileLayer};
|
||||
use crate::{
|
||||
@@ -265,6 +266,12 @@ pub struct SectionMesh {
|
||||
/// model order before.
|
||||
pub sides: Sides,
|
||||
pub translucent: Vec<TerrainVertex>,
|
||||
/// `solid` in the compact layout (`packed`), when the renderer reads it and it is
|
||||
/// smaller; empty otherwise. Made by `sort_solid`.
|
||||
pub packed: Vec<[u32; 6]>,
|
||||
/// The tint of each solid quad as the mesher multiplied it (`packed::UNKNOWN_TINT` where
|
||||
/// it does not say), for `packed`; dropped once that is made.
|
||||
pub(crate) solid_tints: Vec<u32>,
|
||||
/// VisGraph: bit `a * 6 + b` is set when faces `a` and `b` (Direction
|
||||
/// order) are connected through non-opaque blocks.
|
||||
pub visibility: u64,
|
||||
@@ -290,6 +297,8 @@ impl Default for SectionMesh {
|
||||
solid: Vec::new(),
|
||||
sides: Sides::default(),
|
||||
translucent: Vec::new(),
|
||||
packed: Vec::new(),
|
||||
solid_tints: Vec::new(),
|
||||
visibility: ALL_VISIBLE,
|
||||
block_entities: Vec::new(),
|
||||
lights: Vec::new(),
|
||||
@@ -322,6 +331,8 @@ impl SectionMesh {
|
||||
pub fn clear(&mut self) {
|
||||
self.solid.clear();
|
||||
self.translucent.clear();
|
||||
self.packed.clear();
|
||||
self.solid_tints.clear();
|
||||
self.block_entities.clear();
|
||||
self.lights.clear();
|
||||
self.sprite_layers.clear();
|
||||
@@ -333,15 +344,27 @@ impl SectionMesh {
|
||||
pub(crate) fn capacity_bytes(&self) -> usize {
|
||||
self.solid.capacity() * std::mem::size_of::<TerrainVertex>()
|
||||
+ self.translucent.capacity() * std::mem::size_of::<TerrainVertex>()
|
||||
+ self.packed.capacity() * std::mem::size_of::<[u32; 6]>()
|
||||
+ self.solid_tints.capacity() * std::mem::size_of::<u32>()
|
||||
+ self.block_entities.capacity() * std::mem::size_of::<([i32; 3], u32)>()
|
||||
+ self.lights.capacity() * std::mem::size_of::<([i32; 3], u32)>()
|
||||
+ self.sprite_layers.capacity() * std::mem::size_of::<u16>()
|
||||
}
|
||||
/// Sorts the solid quads by the side they face, for the renderer to
|
||||
/// leave out those facing away from the eye (`sides`).
|
||||
/// Then lays them out compactly (`packed`) while the renderer reads that.
|
||||
pub fn sort_solid(&mut self) {
|
||||
self.sides = sides::sort(&mut self.solid);
|
||||
let quads = self.solid.len() / 4;
|
||||
if !self.solid_tints.is_empty() {
|
||||
self.solid_tints.resize(quads, packed::UNKNOWN_TINT);
|
||||
}
|
||||
self.sides = sides::sort_tinted(&mut self.solid, &mut self.solid_tints);
|
||||
self.bounds = self.compute_bounds();
|
||||
self.packed.clear();
|
||||
if packed::packing() && quads > 0 && self.solid_tints.len() == quads {
|
||||
self.packed = packed::layout(&self.solid, &self.solid_tints);
|
||||
}
|
||||
self.solid_tints.clear();
|
||||
}
|
||||
|
||||
/// Computes the tight axis-aligned bounding box [min, max] of actual terrain geometry vertices.
|
||||
@@ -1782,8 +1805,8 @@ impl Mesher<'_> {
|
||||
TileLayer::Skip | TileLayer::Default => own,
|
||||
};
|
||||
match pick {
|
||||
Pick::Keep => self.quad(out, solid_leaves, own, corners.map(|c| c.0)),
|
||||
Pick::Tile(t) => self.quad(out, solid_leaves, tile(t), corners.map(|c| c.0)),
|
||||
Pick::Keep => self.quad(out, solid_leaves, own, corners.map(|c| c.0), tint),
|
||||
Pick::Tile(t) => self.quad(out, solid_leaves, tile(t), corners.map(|c| c.0), tint),
|
||||
Pick::Quarters(tiles) => {
|
||||
let cuts = cuts(&corners);
|
||||
for s in cuts.windows(2) {
|
||||
@@ -1791,7 +1814,8 @@ impl Mesher<'_> {
|
||||
let piece = [(s[0], t[0]), (s[1], t[0]), (s[1], t[1]), (s[0], t[1])]
|
||||
.map(|(s, t)| bilinear(&corners, s, t));
|
||||
let quarter = quarter_of(&piece);
|
||||
self.quad(out, solid_leaves, tile(tiles[quarter]), piece.map(|c| c.0));
|
||||
let piece = piece.map(|c| c.0);
|
||||
self.quad(out, solid_leaves, tile(tiles[quarter]), piece, tint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1835,7 +1859,7 @@ impl Mesher<'_> {
|
||||
for t in cuts.windows(2) {
|
||||
let piece = [(s[0], t[0]), (s[1], t[0]), (s[1], t[1]), (s[0], t[1])]
|
||||
.map(|(s, t)| bilinear(&whole, s, t).0);
|
||||
self.quad_as(out, false, tile, piece, false);
|
||||
self.quad_as(out, false, tile, piece, false, tint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1848,8 +1872,9 @@ impl Mesher<'_> {
|
||||
solid_leaves: bool,
|
||||
tile: TileLayer,
|
||||
v: [TerrainVertex; 4],
|
||||
tint: Option<u32>,
|
||||
) {
|
||||
self.quad_as(out, solid_leaves, tile, v, true);
|
||||
self.quad_as(out, solid_leaves, tile, v, true, tint);
|
||||
}
|
||||
/// Whether [`Mesher::quad`] draws `tile` in the translucent pass.
|
||||
fn translucent(&self, tile: TileLayer) -> bool {
|
||||
@@ -1868,7 +1893,8 @@ impl Mesher<'_> {
|
||||
}
|
||||
}
|
||||
/// [`Mesher::quad`], with the sprite's own translucency (`translucent_layers`) counted only
|
||||
/// when `by_sprite`: an overlay's pass is its rule's.
|
||||
/// when `by_sprite`: an overlay's pass is its rule's. `tint`: what its grey levels were
|
||||
/// multiplied by, for the compact layout (`packed`).
|
||||
fn quad_as(
|
||||
&self,
|
||||
out: &mut SectionMesh,
|
||||
@@ -1876,6 +1902,7 @@ impl Mesher<'_> {
|
||||
tile: TileLayer,
|
||||
mut v: [TerrainVertex; 4],
|
||||
by_sprite: bool,
|
||||
tint: Option<u32>,
|
||||
) {
|
||||
let TileLayer::Layer {
|
||||
layer,
|
||||
@@ -1909,6 +1936,12 @@ impl Mesher<'_> {
|
||||
vertex.layer = layer;
|
||||
}
|
||||
target.extend(v);
|
||||
if !translucent {
|
||||
// Quads other code put in `solid` (lava) have no tint known.
|
||||
let quad = out.solid.len() / 4 - 1;
|
||||
out.solid_tints.resize(quad, packed::UNKNOWN_TINT);
|
||||
out.solid_tints.push(tint.unwrap_or(packed::WHITE));
|
||||
}
|
||||
}
|
||||
}
|
||||
/// The cuts of a `ctm_compact` face, in quad parameters, without allocating: the first `.1`
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
//! The compact layout of a section's solid quads in the arena: most of them are whole faces
|
||||
//! of a block's cube on the block grid, and those take one 24-byte record each instead of
|
||||
//! four 24-byte vertices. terrain.wgsl's `section_corner` expands a record into the very
|
||||
//! vertex words `TerrainVertex` would hold (`packed_vertex` here is the same on the CPU, and
|
||||
//! a record is kept only when it gives back the quad's four vertices bit for bit), so every
|
||||
//! pass draws the same picture from either layout.
|
||||
//!
|
||||
//! A mesh's solid range is then one record per quad in mesh order (so `Sides` and the draws
|
||||
//! count quads as before), padding to a multiple of 4 slots, and the quads no record fits as
|
||||
//! their four vertices: their record holds the distance to those (`FULL`). A record:
|
||||
//!
|
||||
//! - word 0: the tint (0xRRGGBB as the mesher multiplies it) and the colour's alpha byte;
|
||||
//! - word 1: each corner's grey level before the tint;
|
||||
//! - word 2: the UV rectangle in 1/32 of the sprite (u0, u1, v0, v1, 6 bits each) and, per
|
||||
//! corner, which edge of it its u and v take;
|
||||
//! - word 3: the layer, and corner 0's light (sky and block light over 4, 6 bits each);
|
||||
//! - word 4: corners 1 and 2's light;
|
||||
//! - word 5: corner 3's light, the face (block x, y, z, the axis it faces along, which side
|
||||
//! of the block, and where and which way its corners go round), and `FULL` clear.
|
||||
use super::TerrainVertex;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
/// A solid quad's tint the mesher does not know (fluids): only untinted colours pack.
|
||||
pub const UNKNOWN_TINT: u32 = u32::MAX;
|
||||
/// The tint of an untinted quad, whose colour is its grey level.
|
||||
pub const WHITE: u32 = 0xFF_FFFF;
|
||||
/// Word 5's bit of a quad kept as four vertices; word 0 is then the slots from the record
|
||||
/// to its first vertex.
|
||||
pub const FULL: u32 = 1 << 31;
|
||||
/// The draws' base vertex of a compact mesh: `PACKED_BASE + 4 * slot + corner` is a corner of
|
||||
/// the quad whose record is at `slot`; below it, vertex indices name vertices.
|
||||
pub const PACKED_BASE: u32 = 1 << 28;
|
||||
|
||||
/// Whether meshes sorted from now on get the compact layout: set by the renderer while its
|
||||
/// passes read it (no shader pack).
|
||||
static PACK: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub fn pack_quads(on: bool) {
|
||||
PACK.store(on, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn packing() -> bool {
|
||||
PACK.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// The order corners go round a face: (du, dw) of each step.
|
||||
const ROUND: [[u32; 2]; 4] = [[0, 0], [1, 0], [1, 1], [0, 1]];
|
||||
|
||||
/// `quad`'s record, if one gives back its four vertices exactly. `tint`: what the mesher
|
||||
/// multiplied its grey levels by (`UNKNOWN_TINT` when it does not say).
|
||||
pub fn pack_quad(quad: &[TerrainVertex; 4], tint: u32) -> Option<[u32; 6]> {
|
||||
// Corners on the block grid of the section.
|
||||
let mut p = [[0u32; 3]; 4];
|
||||
for (to, v) in p.iter_mut().zip(quad) {
|
||||
for (c, &x) in to.iter_mut().zip(&v.pos) {
|
||||
if !(0.0..=16.0).contains(&x) || x.fract() != 0.0 {
|
||||
return None;
|
||||
}
|
||||
*c = x as u32;
|
||||
}
|
||||
}
|
||||
let axis = (0..3).find(|&a| p.iter().all(|c| c[a] == p[0][a]))?;
|
||||
let (u, w) = ((axis + 1) % 3, (axis + 2) % 3);
|
||||
let lo = [u, w].map(|k| p.iter().map(|c| c[k]).min().unwrap_or(0));
|
||||
if lo.iter().any(|&l| l > 15) {
|
||||
return None;
|
||||
}
|
||||
let mut steps = [0u32; 4];
|
||||
for (step, c) in steps.iter_mut().zip(&p) {
|
||||
let d = [c[u] - lo[0], c[w] - lo[1]];
|
||||
*step = ROUND.iter().position(|r| *r == d)? as u32;
|
||||
}
|
||||
let (start, dir) = (0..8u32).map(|n| (n & 3, n >> 2)).find(|&(start, dir)| {
|
||||
(0..4u32).all(|i| {
|
||||
let step = if dir == 1 { 4 - i } else { i };
|
||||
steps[i as usize] == (start + step) & 3
|
||||
})
|
||||
})?;
|
||||
let plane = p[0][axis];
|
||||
let mut block = [0u32; 3];
|
||||
block[axis] = plane.min(15);
|
||||
block[u] = lo[0];
|
||||
block[w] = lo[1];
|
||||
let side = plane - block[axis];
|
||||
let orient = block[0]
|
||||
| block[1] << 4
|
||||
| block[2] << 8
|
||||
| (axis as u32) << 12
|
||||
| side << 14
|
||||
| start << 15
|
||||
| dir << 17;
|
||||
// UVs in 1/32 of the sprite, at most two values each way.
|
||||
let mut ranges = [[u32::MAX, 0u32]; 2];
|
||||
let mut uvs = [[0u32; 2]; 4];
|
||||
for (uv, v) in uvs.iter_mut().zip(quad) {
|
||||
for k in 0..2 {
|
||||
let raw = u32::from(v.uv[k]);
|
||||
if raw & 1023 != 0 {
|
||||
return None;
|
||||
}
|
||||
uv[k] = raw >> 10;
|
||||
ranges[k] = [ranges[k][0].min(uv[k]), ranges[k][1].max(uv[k])];
|
||||
}
|
||||
}
|
||||
let mut select = 0u32;
|
||||
for (i, uv) in uvs.iter().enumerate() {
|
||||
for k in 0..2 {
|
||||
if uv[k] == ranges[k][1] && uv[k] != ranges[k][0] {
|
||||
select |= 1 << (2 * i + k);
|
||||
} else if uv[k] != ranges[k][0] {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
let rect = ranges[0][0] | ranges[0][1] << 6 | ranges[1][0] << 12 | ranges[1][1] << 18;
|
||||
// Light in steps of 4 (smooth light averages four levels of 16).
|
||||
let mut light = [0u32; 4];
|
||||
for (l, v) in light.iter_mut().zip(quad) {
|
||||
let (sky, block) = (u32::from(v.light & 0xFF), u32::from(v.light >> 8));
|
||||
if sky & 3 != 0 || block & 3 != 0 {
|
||||
return None;
|
||||
}
|
||||
*l = sky >> 2 | (block >> 2) << 6;
|
||||
}
|
||||
// The colour: each channel the grey level times the tint's, over 255.
|
||||
let tint = if tint == UNKNOWN_TINT {
|
||||
WHITE
|
||||
} else {
|
||||
tint & WHITE
|
||||
};
|
||||
let alpha = quad[0].color & 0xFF00_0000;
|
||||
let mut grays = 0u32;
|
||||
for (i, v) in quad.iter().enumerate() {
|
||||
grays |= gray_of(v.color, tint)? << (8 * i);
|
||||
}
|
||||
let words = [
|
||||
tint | alpha,
|
||||
grays,
|
||||
rect | select << 24,
|
||||
u32::from(quad[0].layer) | light[0] << 16,
|
||||
light[1] | light[2] << 12,
|
||||
light[3] | orient << 12,
|
||||
];
|
||||
let exact = (0..4).all(|c| {
|
||||
let (a, b) = (packed_vertex(&words, c), quad[c]);
|
||||
bytemuck::bytes_of(&a) == bytemuck::bytes_of(&b)
|
||||
});
|
||||
exact.then_some(words)
|
||||
}
|
||||
|
||||
/// The tint's channels, as the mesher takes them for red, green and blue.
|
||||
fn channels(tint: u32) -> [u32; 3] {
|
||||
[tint >> 16 & 255, tint >> 8 & 255, tint & 255]
|
||||
}
|
||||
|
||||
/// A grey level that `tint` turns into `color`'s red, green and blue.
|
||||
fn gray_of(color: u32, tint: u32) -> Option<u32> {
|
||||
let want = [color & 255, color >> 8 & 255, color >> 16 & 255];
|
||||
let t = channels(tint);
|
||||
let gives = |g: u32| (0..3).all(|k| g * t[k] / 255 == want[k]);
|
||||
let k = (0..3).max_by_key(|&k| t[k]).unwrap_or(0);
|
||||
if t[k] == 0 {
|
||||
return gives(0).then_some(0);
|
||||
}
|
||||
let lo = (want[k] * 255).div_ceil(t[k]);
|
||||
let hi = ((want[k] * 255 + 254) / t[k]).min(255);
|
||||
(lo..=hi).find(|&g| gives(g))
|
||||
}
|
||||
|
||||
/// Corner `corner` of the quad `words` holds, as terrain.wgsl's `packed_corner` expands it.
|
||||
pub fn packed_vertex(words: &[u32; 6], corner: usize) -> TerrainVertex {
|
||||
let [w0, w1, w2, w3, w4, w5] = *words;
|
||||
let orient = w5 >> 12 & 0x3_FFFF;
|
||||
let block = [orient & 15, orient >> 4 & 15, orient >> 8 & 15];
|
||||
let axis = (orient >> 12 & 3) as usize;
|
||||
let side = orient >> 14 & 1;
|
||||
let (start, dir) = (orient >> 15 & 3, orient >> 17 & 1);
|
||||
let i = corner as u32;
|
||||
let step = (start + if dir == 1 { 4 - i } else { i }) & 3;
|
||||
let [du, dw] = ROUND[step as usize];
|
||||
let mut pos = block;
|
||||
pos[axis] += side;
|
||||
pos[(axis + 1) % 3] += du;
|
||||
pos[(axis + 2) % 3] += dw;
|
||||
let select = w2 >> 24;
|
||||
let us = select >> (2 * i) & 1;
|
||||
let vs = select >> (2 * i + 1) & 1;
|
||||
let uv = [w2 >> (6 * us) & 63, w2 >> (12 + 6 * vs) & 63].map(|k| (k << 10) as u16);
|
||||
let light = [w3 >> 16, w4, w4 >> 12, w5][corner] & 0xFFF;
|
||||
let light = ((light & 63) << 2 | (light >> 6 & 63) << 10) as u16;
|
||||
let g = w1 >> (8 * i) & 255;
|
||||
let [r, gr, b] = channels(w0).map(|t| g * t / 255);
|
||||
TerrainVertex {
|
||||
pos: pos.map(|c| c as f32),
|
||||
uv,
|
||||
layer: (w3 & 0xFFFF) as u16,
|
||||
light,
|
||||
color: r | gr << 8 | b << 16 | (w0 & 0xFF00_0000),
|
||||
}
|
||||
}
|
||||
|
||||
/// The compact layout of sorted solid quads with their tints (`SectionMesh::packed`), or
|
||||
/// nothing when it would be no smaller than their vertices.
|
||||
pub fn layout(solid: &[TerrainVertex], tints: &[u32]) -> Vec<[u32; 6]> {
|
||||
let quads = solid.as_chunks::<4>().0;
|
||||
let records: Vec<Option<[u32; 6]>> = quads
|
||||
.iter()
|
||||
.zip(tints)
|
||||
.map(|(q, &t)| pack_quad(q, t))
|
||||
.collect();
|
||||
let n = quads.len();
|
||||
let packed = records.iter().filter(|r| r.is_some()).count();
|
||||
let full = n - packed;
|
||||
let pad = if full > 0 {
|
||||
n.next_multiple_of(4) - n
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if n + pad + 4 * full >= 4 * n {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut out = Vec::with_capacity(n + pad + 4 * full);
|
||||
let mut next = n + pad;
|
||||
for (i, record) in records.iter().enumerate() {
|
||||
out.push(record.unwrap_or_else(|| {
|
||||
let at = next;
|
||||
next += 4;
|
||||
[(at - i) as u32, 0, 0, 0, 0, FULL]
|
||||
}));
|
||||
}
|
||||
out.resize(n + pad, [0; 6]);
|
||||
for (q, record) in quads.iter().zip(&records) {
|
||||
if record.is_none() {
|
||||
out.extend(
|
||||
q.iter()
|
||||
.map(|v| bytemuck::cast::<TerrainVertex, [u32; 6]>(*v)),
|
||||
);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Quads `layout` gave a record of their own.
|
||||
pub fn packed_quads(layout: &[[u32; 6]], quads: usize) -> usize {
|
||||
layout
|
||||
.iter()
|
||||
.take(quads)
|
||||
.filter(|w| w[5] & FULL == 0)
|
||||
.count()
|
||||
}
|
||||
@@ -205,13 +205,21 @@ fn bucket(quad: &[TerrainVertex; 4]) -> usize {
|
||||
/// cutout quads are also ordered front to back within each axis-facing
|
||||
/// bucket; quads that tie keep the mesher's order (the quad's index ends its
|
||||
/// sort key), so coplanar overlays still draw over their base.
|
||||
pub fn sort(solid: &mut Vec<TerrainVertex>) -> Sides {
|
||||
sort_parts(solid, GROUPED.load(Ordering::Relaxed))
|
||||
#[cfg(test)]
|
||||
fn sort(solid: &mut Vec<TerrainVertex>) -> Sides {
|
||||
sort_tinted(solid, &mut Vec::new())
|
||||
}
|
||||
|
||||
/// `sort`, moving each quad's tint in `tints` (one per quad, or none) with it.
|
||||
pub fn sort_tinted(solid: &mut Vec<TerrainVertex>, tints: &mut Vec<u32>) -> Sides {
|
||||
let grouped = GROUPED.load(Ordering::Relaxed);
|
||||
SORT_SCRATCH.with_borrow_mut(|scratch| sort_with(solid, tints, grouped, scratch))
|
||||
}
|
||||
|
||||
/// `sort`, with all the opaque parts first when `grouped`.
|
||||
#[cfg(test)]
|
||||
fn sort_parts(solid: &mut Vec<TerrainVertex>, grouped: bool) -> Sides {
|
||||
SORT_SCRATCH.with_borrow_mut(|scratch| sort_with(solid, grouped, scratch))
|
||||
SORT_SCRATCH.with_borrow_mut(|scratch| sort_with(solid, &mut Vec::new(), grouped, scratch))
|
||||
}
|
||||
|
||||
/// `sort`'s buffers, kept by each meshing thread so sorting a mesh allocates
|
||||
@@ -222,6 +230,7 @@ struct SortScratch {
|
||||
slots: Vec<u8>,
|
||||
keys: Vec<u64>,
|
||||
sorted: Vec<[TerrainVertex; 4]>,
|
||||
tints: Vec<u32>,
|
||||
}
|
||||
thread_local! {
|
||||
static SORT_SCRATCH: std::cell::RefCell<SortScratch> = Default::default();
|
||||
@@ -233,9 +242,15 @@ fn total_order_key(v: f32) -> u32 {
|
||||
(bits ^ (((bits >> 31) as u32) >> 1) as i32) as u32 ^ 0x8000_0000
|
||||
}
|
||||
|
||||
fn sort_with(solid: &mut Vec<TerrainVertex>, grouped: bool, scratch: &mut SortScratch) -> Sides {
|
||||
fn sort_with(
|
||||
solid: &mut Vec<TerrainVertex>,
|
||||
tints: &mut Vec<u32>,
|
||||
grouped: bool,
|
||||
scratch: &mut SortScratch,
|
||||
) -> Sides {
|
||||
let (quads, rest) = solid.as_chunks::<4>();
|
||||
debug_assert!(rest.is_empty(), "solid vertices come in quads");
|
||||
let quads_len = quads.len();
|
||||
let cutout = |q: &[TerrainVertex; 4]| q[0].layer & OPAQUE_LAYER == 0;
|
||||
// Each quad's part, by its place in mesh order (`slot`).
|
||||
let position = |b: usize, cutout: bool| match grouped {
|
||||
@@ -297,6 +312,12 @@ fn sort_with(solid: &mut Vec<TerrainVertex>, grouped: bool, scratch: &mut SortSc
|
||||
sorted.extend(keys.iter().map(|&k| quads[(k & 0xFF_FFFF) as usize]));
|
||||
solid.clear();
|
||||
solid.extend_from_slice(sorted.as_flattened());
|
||||
if tints.len() == quads_len {
|
||||
let sorted_tints = &mut scratch.tints;
|
||||
sorted_tints.clear();
|
||||
sorted_tints.extend(keys.iter().map(|&k| tints[(k & 0xFF_FFFF) as usize]));
|
||||
std::mem::swap(tints, sorted_tints);
|
||||
}
|
||||
}
|
||||
Sides {
|
||||
ends: slot_ends,
|
||||
|
||||
@@ -398,6 +398,134 @@ fn left_out() -> TerrainOut {
|
||||
return terrain_vertex(position, terrain_uv(uv, origin.w), layer, color, light, origin.xyz,
|
||||
origin.w);
|
||||
}
|
||||
// Solid section draws that read their vertices from the arena page (gfx.rs's `pulled`
|
||||
// pipelines; `mesher/packed.rs`): a draw's vertex indices below PACKED_BASE name vertices
|
||||
// (`TerrainVertex`, six words, as `TERRAIN_ATTRIBUTES` reads them), from PACKED_BASE on a
|
||||
// corner of the quad whose record is at that slot (`PACKED_BASE + 4 * slot + corner`).
|
||||
// Either way they come out as the six words of the vertex, so the work after is the same.
|
||||
// Binding 19: visbuffer.wgsl, in the same module, takes 2 to 18 of its own group 1.
|
||||
@group(1) @binding(19) var<storage, read> arena_page: array<vec2<u32>>;
|
||||
const PACKED_BASE: u32 = 0x10000000u;
|
||||
// A vertex's words: position bits, then UV, layer with light, colour.
|
||||
struct ArenaCorner {
|
||||
p: vec3<u32>,
|
||||
r: vec3<u32>,
|
||||
}
|
||||
fn arena_vertex(v: u32) -> ArenaCorner {
|
||||
let a = arena_page[v * 3u];
|
||||
let b = arena_page[v * 3u + 1u];
|
||||
let c = arena_page[v * 3u + 2u];
|
||||
return ArenaCorner(vec3(a.x, a.y, b.x), vec3(b.y, c.x, c.y));
|
||||
}
|
||||
// `packed::packed_vertex`: corner `corner` of the record whose words are lo (0 to 2) and
|
||||
// hi (3 to 5).
|
||||
fn packed_corner(lo: vec3<u32>, hi: vec3<u32>, corner: u32) -> ArenaCorner {
|
||||
let orient = (hi.z >> 12u) & 0x3ffffu;
|
||||
let block = vec3(orient & 15u, (orient >> 4u) & 15u, (orient >> 8u) & 15u);
|
||||
let axis = (orient >> 12u) & 3u;
|
||||
let side = (orient >> 14u) & 1u;
|
||||
let start = (orient >> 15u) & 3u;
|
||||
let dir = (orient >> 17u) & 1u;
|
||||
let step = (start + select(corner, 4u - corner, dir == 1u)) & 3u;
|
||||
let du = select(0u, 1u, step == 1u || step == 2u);
|
||||
let dw = select(0u, 1u, step >= 2u);
|
||||
var pos = block;
|
||||
pos[axis] = block[axis] + side;
|
||||
pos[(axis + 1u) % 3u] = block[(axis + 1u) % 3u] + du;
|
||||
pos[(axis + 2u) % 3u] = block[(axis + 2u) % 3u] + dw;
|
||||
let sel = lo.z >> 24u;
|
||||
let us = (sel >> (2u * corner)) & 1u;
|
||||
let vs = (sel >> (2u * corner + 1u)) & 1u;
|
||||
let u = (lo.z >> (6u * us)) & 63u;
|
||||
let v = (lo.z >> (12u + 6u * vs)) & 63u;
|
||||
var light = hi.z;
|
||||
if corner == 0u {
|
||||
light = hi.x >> 16u;
|
||||
} else if corner == 1u {
|
||||
light = hi.y;
|
||||
} else if corner == 2u {
|
||||
light = hi.y >> 12u;
|
||||
}
|
||||
light &= 0xfffu;
|
||||
let packed_light = ((light & 63u) << 2u) | (((light >> 6u) & 63u) << 10u);
|
||||
let g = (lo.y >> (8u * corner)) & 255u;
|
||||
let t = lo.x;
|
||||
let color = (g * ((t >> 16u) & 255u) / 255u) | ((g * ((t >> 8u) & 255u) / 255u) << 8u)
|
||||
| ((g * (t & 255u) / 255u) << 16u) | (t & 0xff000000u);
|
||||
return ArenaCorner(bitcast<vec3<u32>>(vec3<f32>(pos)),
|
||||
vec3((u << 10u) | (v << 26u), (hi.x & 0xffffu) | (packed_light << 16u), color));
|
||||
}
|
||||
// A solid draw's vertex `vertex`, and the slot its block is found by (`block_slots`, 256
|
||||
// slots a block): the vertex's own, or its quad's record's. `packed`: it has a record.
|
||||
struct SectionCorner {
|
||||
c: ArenaCorner,
|
||||
slot: u32,
|
||||
packed: bool,
|
||||
}
|
||||
fn section_corner(vertex: u32) -> SectionCorner {
|
||||
if vertex < PACKED_BASE {
|
||||
return SectionCorner(arena_vertex(vertex), vertex, false);
|
||||
}
|
||||
let slot = (vertex - PACKED_BASE) >> 2u;
|
||||
let corner = vertex & 3u;
|
||||
let a = arena_page[slot * 3u];
|
||||
let b = arena_page[slot * 3u + 1u];
|
||||
let c = arena_page[slot * 3u + 2u];
|
||||
// `packed::FULL`: the quad's four vertices, word 0 slots on.
|
||||
if (c.y & 0x80000000u) != 0u {
|
||||
return SectionCorner(arena_vertex(slot + a.x + corner), slot, true);
|
||||
}
|
||||
return SectionCorner(packed_corner(vec3(a.x, a.y, b.x), vec3(b.y, c.x, c.y), corner), slot,
|
||||
true);
|
||||
}
|
||||
// The colour as the vertex fetch converts a Unorm8x4 attribute, bit for bit (Apple's M1:
|
||||
// times 1/255; `unpack4x8unorm` and the correctly rounded c / 255 each differ from it for
|
||||
// some bytes, and moved a few pixels' colour or alpha cutout).
|
||||
fn unorm8x4(c: u32) -> vec4<f32> {
|
||||
return vec4<f32>(vec4(c & 255u, (c >> 8u) & 255u, (c >> 16u) & 255u, c >> 24u))
|
||||
* (1.0 / 255.0);
|
||||
}
|
||||
fn corner_layer(c: ArenaCorner) -> u32 {
|
||||
return c.r.y & 0xffffu;
|
||||
}
|
||||
fn corner_vertex(c: ArenaCorner, origin: vec4<f32>) -> TerrainOut {
|
||||
let uv = vec2(c.r.x & 0xffffu, c.r.x >> 16u);
|
||||
let light = vec2((c.r.y >> 16u) & 0xffu, c.r.y >> 24u);
|
||||
return terrain_vertex(bitcast<vec3<f32>>(c.p), terrain_uv(uv, origin.w), corner_layer(c),
|
||||
unorm8x4(c.r.z), light, origin.xyz, origin.w);
|
||||
}
|
||||
@vertex fn vs_section(@builtin(vertex_index) vertex: u32,
|
||||
@location(5) origin: vec4<f32>) -> TerrainOut {
|
||||
return corner_vertex(section_corner(vertex).c, origin);
|
||||
}
|
||||
@vertex fn vs_section_opaque(@builtin(vertex_index) vertex: u32,
|
||||
@location(5) origin: vec4<f32>) -> TerrainOut {
|
||||
let s = section_corner(vertex);
|
||||
if !opaque_quad(corner_layer(s.c), origin.w) { return left_out(); }
|
||||
return corner_vertex(s.c, origin);
|
||||
}
|
||||
@vertex fn vs_section_cutout(@builtin(vertex_index) vertex: u32,
|
||||
@location(5) origin: vec4<f32>) -> TerrainOut {
|
||||
let s = section_corner(vertex);
|
||||
if opaque_quad(corner_layer(s.c), origin.w) { return left_out(); }
|
||||
return corner_vertex(s.c, origin);
|
||||
}
|
||||
@vertex fn vs_section_flat(@builtin(vertex_index) vertex: u32) -> TerrainOut {
|
||||
let s = section_corner(vertex);
|
||||
return corner_vertex(s.c, slot_origins[block_slots[s.slot >> 8u]]);
|
||||
}
|
||||
@vertex fn vs_section_flat_opaque(@builtin(vertex_index) vertex: u32) -> TerrainOut {
|
||||
let s = section_corner(vertex);
|
||||
let origin = slot_origins[block_slots[s.slot >> 8u]];
|
||||
if !opaque_quad(corner_layer(s.c), origin.w) { return left_out(); }
|
||||
return corner_vertex(s.c, origin);
|
||||
}
|
||||
@vertex fn vs_section_flat_cutout(@builtin(vertex_index) vertex: u32) -> TerrainOut {
|
||||
let s = section_corner(vertex);
|
||||
let origin = slot_origins[block_slots[s.slot >> 8u]];
|
||||
if opaque_quad(corner_layer(s.c), origin.w) { return left_out(); }
|
||||
return corner_vertex(s.c, origin);
|
||||
}
|
||||
// HDR lighting: how far a surface's light carries it above paper white, 0 to 1. Only the
|
||||
// strongest light does, so dark stays as dark as in SDR; sky light by how the face meets the sun.
|
||||
fn hdr_scene(block: f32, sky: f32, facing: f32) -> f32 {
|
||||
|
||||
@@ -8,10 +8,16 @@
|
||||
// and works out the screen gradients `dpdx`/`dpdy` would give, so the texel, mip level, light
|
||||
// and fog come out as in the normal path.
|
||||
|
||||
// Bit 31: terrain is here; bits 22 to 30: the arena page; bits 0 to 21: the quad in the page
|
||||
// (a page holds at most `arena::PAGE_BYTES` / 24 bytes = 2 796 202 quads, under 2^22).
|
||||
fn vis_id(page: u32, vertex: u32) -> u32 {
|
||||
return 0x80000000u | ((page & 0x1ffu) << 22u) | (vertex >> 2u);
|
||||
// Bit 31: terrain is here; bit 30: the quad has a record (`mesher/packed.rs`); bits 24 to 29:
|
||||
// the arena page (`visbuffer::MAX_PAGES`); bits 0 to 23: the quad's record's slot in the page,
|
||||
// else its first vertex over 4 (a page holds at most `arena::PAGE_BYTES` / 24 bytes =
|
||||
// 11 184 810 slots, under 2^24).
|
||||
fn vis_id(page: u32, s: SectionCorner) -> u32 {
|
||||
let at = 0x80000000u | ((page & 0x3fu) << 24u);
|
||||
if s.packed {
|
||||
return at | 0x40000000u | s.slot;
|
||||
}
|
||||
return at | (s.slot >> 2u);
|
||||
}
|
||||
|
||||
// `sprite_at` (terrain.wgsl's `sample_rgss` over `sample_nearest`), sampling only what its
|
||||
@@ -68,18 +74,18 @@ struct VisIdOut {
|
||||
// keeps hiding what lies behind them. `page`: the draw's instance, its arena page
|
||||
// (`Flat::draw_paged`).
|
||||
@vertex fn vs_vis_opaque(
|
||||
@builtin(vertex_index) vertex: u32, @builtin(instance_index) page: u32,
|
||||
@location(0) position: vec3<f32>, @location(1) uv: vec2<u32>,
|
||||
@location(2) layer: u32, @location(3) color: vec4<f32>, @location(4) light: vec2<u32>
|
||||
@builtin(vertex_index) vertex: u32, @builtin(instance_index) page: u32
|
||||
) -> VisIdOut {
|
||||
var out: VisIdOut;
|
||||
let origin = slot_origins[block_slots[vertex >> 8u]];
|
||||
if !opaque_quad(layer, origin.w) {
|
||||
let s = section_corner(vertex);
|
||||
let origin = slot_origins[block_slots[s.slot >> 8u]];
|
||||
if !opaque_quad(corner_layer(s.c), origin.w) {
|
||||
out.clip = vec4(2.0, 2.0, 2.0, 1.0);
|
||||
return out;
|
||||
}
|
||||
let position = bitcast<vec3<f32>>(s.c.p);
|
||||
out.clip = fade_depth(globals.view_proj * vec4(position + origin.xyz, 1.0), origin.w);
|
||||
out.id = vis_id(page, vertex);
|
||||
out.id = vis_id(page, s);
|
||||
return out;
|
||||
}
|
||||
@fragment fn fs_vis_id(in: VisIdOut) -> @location(0) u32 {
|
||||
@@ -98,19 +104,22 @@ struct VisCutoutOut {
|
||||
}
|
||||
// The other quads: `solid_color`'s fade dither and alpha cutout, with no shading.
|
||||
@vertex fn vs_vis_cutout(
|
||||
@builtin(vertex_index) vertex: u32, @builtin(instance_index) page: u32,
|
||||
@location(0) position: vec3<f32>, @location(1) uv: vec2<u32>,
|
||||
@location(2) layer: u32, @location(3) color: vec4<f32>, @location(4) light: vec2<u32>
|
||||
@builtin(vertex_index) vertex: u32, @builtin(instance_index) page: u32
|
||||
) -> VisCutoutOut {
|
||||
var out: VisCutoutOut;
|
||||
let origin = slot_origins[block_slots[vertex >> 8u]];
|
||||
let s = section_corner(vertex);
|
||||
let origin = slot_origins[block_slots[s.slot >> 8u]];
|
||||
let layer = corner_layer(s.c);
|
||||
if opaque_quad(layer, origin.w) {
|
||||
out.clip = vec4(2.0, 2.0, 2.0, 1.0);
|
||||
return out;
|
||||
}
|
||||
let position = bitcast<vec3<f32>>(s.c.p);
|
||||
let uv = vec2(s.c.r.x & 0xffffu, s.c.r.x >> 16u);
|
||||
let light = vec2((s.c.r.y >> 16u) & 0xffu, s.c.r.y >> 24u);
|
||||
let pos = position + origin.xyz;
|
||||
out.clip = fade_depth(globals.view_proj * vec4(pos, 1.0), origin.w);
|
||||
out.id = vis_id(page, vertex);
|
||||
out.id = vis_id(page, s);
|
||||
out.uv = terrain_uv(uv, origin.w);
|
||||
out.layer = layer;
|
||||
let block = max(f32(light.y), dynamic_block_light(pos));
|
||||
@@ -276,31 +285,74 @@ fn vis_corner(position: vec3<u32>, rest: vec3<u32>, origin: vec4<f32>) -> Terrai
|
||||
@fragment fn fs_vis_resolve(in: VisResolveOut) -> @location(0) vec4<f32> {
|
||||
let pixel = vec2<i32>(floor(in.clip.xy));
|
||||
let id = textureLoad(vis_ids, pixel, 0).x;
|
||||
let page = (id >> 22u) & 0x1ffu;
|
||||
let page = (id >> 24u) & 0x3fu;
|
||||
if (id & 0x80000000u) == 0u || page / 16u != in.chunk {
|
||||
discard;
|
||||
}
|
||||
let slot = page % 16u;
|
||||
let quad = id & 0x3fffffu;
|
||||
let at = 6u * quad;
|
||||
// The quad's block (256 vertices, 64 quads) names its section, as for `vs_terrain_flat`.
|
||||
let origin = slot_origins[block_slots[page * VIS_BLOCKS_PER_PAGE + (quad >> 6u)]];
|
||||
// Corner k is words 6k to 6k + 5 of the quad's 24.
|
||||
let words = vis_quad(slot, at).q;
|
||||
let q0 = words[0];
|
||||
let q1 = words[1];
|
||||
let q2 = words[2];
|
||||
let q3 = words[3];
|
||||
let q4 = words[4];
|
||||
let q5 = words[5];
|
||||
let p0 = q0.xyz;
|
||||
let r0 = vec3(q0.w, q1.x, q1.y);
|
||||
let p1 = vec3(q1.z, q1.w, q2.x);
|
||||
let r1 = q2.yzw;
|
||||
let p2 = q3.xyz;
|
||||
let r2 = vec3(q3.w, q4.x, q4.y);
|
||||
let p3 = vec3(q4.z, q4.w, q5.x);
|
||||
let r3 = q5.yzw;
|
||||
let index = id & 0xffffffu;
|
||||
// The quad's first vertex (four of them from a multiple of 4) or its record.
|
||||
var first = 4u * index;
|
||||
var record = false;
|
||||
var lo = vec3(0u);
|
||||
var hi = vec3(0u);
|
||||
if (id & 0x40000000u) != 0u {
|
||||
first = index;
|
||||
// The record's six words from word 6 * index: two 16-byte words from the one it
|
||||
// starts in, at its start or its middle.
|
||||
let w = vis_quad(slot, (6u * index) >> 2u).q;
|
||||
let middle = ((6u * index) & 3u) != 0u;
|
||||
lo = select(w[0].xyz, vec3(w[0].zw, w[1].x), middle);
|
||||
hi = select(vec3(w[0].w, w[1].xy), w[1].yzw, middle);
|
||||
record = (hi.z & 0x80000000u) == 0u;
|
||||
if !record {
|
||||
// `packed::FULL`: its four vertices, word 0 slots on.
|
||||
first = index + lo.x;
|
||||
}
|
||||
}
|
||||
// The quad's block (256 slots: 64 quads of vertices, 256 records) names its section, as
|
||||
// for `vs_section_flat`.
|
||||
let block = select(index >> 6u, index >> 8u, (id & 0x40000000u) != 0u);
|
||||
let origin = slot_origins[block_slots[page * VIS_BLOCKS_PER_PAGE + block]];
|
||||
var p0: vec3<u32>;
|
||||
var r0: vec3<u32>;
|
||||
var p1: vec3<u32>;
|
||||
var r1: vec3<u32>;
|
||||
var p2: vec3<u32>;
|
||||
var r2: vec3<u32>;
|
||||
var p3: vec3<u32>;
|
||||
var r3: vec3<u32>;
|
||||
if record {
|
||||
let c0 = packed_corner(lo, hi, 0u);
|
||||
let c1 = packed_corner(lo, hi, 1u);
|
||||
let c2 = packed_corner(lo, hi, 2u);
|
||||
let c3 = packed_corner(lo, hi, 3u);
|
||||
p0 = c0.p;
|
||||
r0 = c0.r;
|
||||
p1 = c1.p;
|
||||
r1 = c1.r;
|
||||
p2 = c2.p;
|
||||
r2 = c2.r;
|
||||
p3 = c3.p;
|
||||
r3 = c3.r;
|
||||
} else {
|
||||
// Corner k is words 6k to 6k + 5 of the quad's 24.
|
||||
let words = vis_quad(slot, 6u * (first >> 2u)).q;
|
||||
let q0 = words[0];
|
||||
let q1 = words[1];
|
||||
let q2 = words[2];
|
||||
let q3 = words[3];
|
||||
let q4 = words[4];
|
||||
let q5 = words[5];
|
||||
p0 = q0.xyz;
|
||||
r0 = vec3(q0.w, q1.x, q1.y);
|
||||
p1 = vec3(q1.z, q1.w, q2.x);
|
||||
r1 = q2.yzw;
|
||||
p2 = q3.xyz;
|
||||
r2 = vec3(q3.w, q4.x, q4.y);
|
||||
p3 = vec3(q4.z, q4.w, q5.x);
|
||||
r3 = q5.yzw;
|
||||
}
|
||||
let c0 = (globals.view_proj * vec4(bitcast<vec3<f32>>(p0) + origin.xyz, 1.0)).xyw;
|
||||
let c1 = (globals.view_proj * vec4(bitcast<vec3<f32>>(p1) + origin.xyz, 1.0)).xyw;
|
||||
let c2 = (globals.view_proj * vec4(bitcast<vec3<f32>>(p2) + origin.xyz, 1.0)).xyw;
|
||||
|
||||
Reference in new issue
Block a user