mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Batch retail material bindings to reduce CPU render commands
This commit is contained in:
1 parent
e0320796c1
commit
bea625f987
11 files changed
+282
-45
No files matched your search
Generated
+2
@@ -4738,6 +4738,8 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"bevy",
|
||||
"half",
|
||||
"naga",
|
||||
"naga_oil",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"skate-core",
|
||||
|
||||
@@ -34,3 +34,7 @@ skate-mods = { path = "../skate-mods" }
|
||||
|
||||
skate-vehicles = { path = "../skate-vehicles" }
|
||||
skate-net = { path = "../skate-net" }
|
||||
|
||||
[dev-dependencies]
|
||||
naga = { version = "=27.0.3", features = ["wgsl-in"] }
|
||||
naga_oil = { version = "=0.20.0", default-features = false }
|
||||
@@ -454,7 +454,7 @@ fn end_frame(
|
||||
"assets",
|
||||
json!({"meshes":meshes.len(),"images":images.len(),"entities":entities.iter().count()}),
|
||||
);
|
||||
capture.send(json!({"name":"configuration","ph":"i","s":"g","pid":1,"tid":0,"ts":capture.origin.elapsed().as_micros() as u64,"args":{"map_fingerprint":format!("{:016x}",config.map_fingerprint),"difficulty":config.difficulty.key(),"timestamp_queries":device.as_ref().is_some_and(|d|d.features().contains(bevy::render::settings::WgpuFeatures::TIMESTAMP_QUERY)),"windows":windows.iter().map(|w|json!({"width":w.physical_width(),"height":w.physical_height(),"present_mode":format!("{:?}",w.present_mode)})).collect::<Vec<_>>(),"cameras":cameras.iter().map(|(c,m)|json!({"size":c.physical_target_size().map(|s|[s.x,s.y]),"msaa":m.samples()})).collect::<Vec<_>>()}}));
|
||||
capture.send(json!({"name":"configuration","ph":"i","s":"g","pid":1,"tid":0,"ts":capture.origin.elapsed().as_micros() as u64,"args":{"map_fingerprint":format!("{:016x}",config.map_fingerprint),"difficulty":config.difficulty.key(),"retail_bindless":device.as_ref().map(|d|bevy::pbr::material_uses_bindless_resources::<crate::retail_render::RetailWorldMaterial>(d)),"timestamp_queries":device.as_ref().is_some_and(|d|d.features().contains(bevy::render::settings::WgpuFeatures::TIMESTAMP_QUERY)),"windows":windows.iter().map(|w|json!({"width":w.physical_width(),"height":w.physical_height(),"present_mode":format!("{:?}",w.present_mode)})).collect::<Vec<_>>(),"cameras":cameras.iter().map(|(c,m)|json!({"size":c.physical_target_size().map(|s|[s.x,s.y]),"msaa":m.samples()})).collect::<Vec<_>>()}}));
|
||||
}
|
||||
for diagnostic in diagnostics.iter() {
|
||||
let path = diagnostic.path().as_str();
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
//! GPU-free validation of the actual material shaders and binding-array access.
|
||||
//! Bevy vertex interfaces are real; unrelated lighting functions/resources use
|
||||
//! interface fixtures. Runtime pipeline/device validation remains a user check.
|
||||
use naga_oil::compose::{Composer, ComposableModuleDescriptor, NagaModuleDescriptor, ShaderDefValue};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn material_index_table_matches_shader_resources() {
|
||||
use bevy::render::render_resource::{AsBindGroup, BindlessResourceType as R};
|
||||
let descriptor = super::RetailWorldMaterial::bindless_descriptor().unwrap();
|
||||
assert_eq!(descriptor.resources.as_ref(), &[
|
||||
R::DataBuffer, R::Texture2d, R::SamplerFiltering,
|
||||
R::Texture2d, R::SamplerFiltering, R::Texture2d, R::SamplerFiltering,
|
||||
R::Texture2d, R::SamplerFiltering, R::Texture2d, R::SamplerFiltering,
|
||||
R::Texture2d, R::SamplerFiltering, R::Texture2d, R::SamplerFiltering,
|
||||
R::TextureCube, R::Buffer,
|
||||
]);
|
||||
assert_eq!(descriptor.index_tables.len(), 1);
|
||||
let table = &descriptor.index_tables[0];
|
||||
assert_eq!((table.binding_number.0, table.indices.start.0, table.indices.end.0), (0, 0, 17));
|
||||
let mut buffers: Vec<_> = descriptor.buffers.iter().map(|b| (b.bindless_index.0, b.binding_number.0)).collect();
|
||||
buffers.sort_unstable();
|
||||
assert_eq!(buffers, [(0, 17), (16, 18)]);
|
||||
}
|
||||
|
||||
fn validate(bindless: bool, prepass: bool, extras: &[&str]) {
|
||||
let mut defs = HashMap::from([("MATERIAL_BIND_GROUP".into(),ShaderDefValue::UInt(3))]);
|
||||
for &name in ["VERTEX_UVS_A","VERTEX_UVS_B","VERTEX_TANGENTS","VERTEX_COLORS","VERTEX_NORMALS","VERTEX_OUTPUT_INSTANCE_INDEX"].iter().chain(extras) {
|
||||
defs.insert(name.into(),ShaderDefValue::Bool(true));
|
||||
}
|
||||
if bindless { defs.insert("BINDLESS".into(),ShaderDefValue::Bool(true)); }
|
||||
let mut composer = Composer::default().with_capabilities(naga::valid::Capabilities::all());
|
||||
let fixtures = [
|
||||
("forward",include_str!("../../../vendor/bevy_pbr/src/render/forward_io.wgsl")),
|
||||
("prepass",include_str!("../../../vendor/bevy_pbr/src/prepass/prepass_io.wgsl")),
|
||||
("mesh", "#define_import_path bevy_pbr::mesh_bindings\nstruct Mesh { material_and_lightmap_bind_group_slot: u32 }\n@group(2) @binding(0) var<storage> mesh: array<Mesh>;"),
|
||||
("frame", "#define_import_path bevy_pbr::mesh_view_bindings\nstruct View {view_from_world:mat4x4<f32>,viewport:vec4<f32>,world_position:vec3<f32>,padding:f32}\nstruct Light {flags:u32}\nstruct Lights {n_directional_lights:u32,directional_lights:array<Light,10>}\n@group(0) @binding(0) var<uniform> view:View;\n@group(0) @binding(1) var<storage> lights:Lights;"),
|
||||
("shadows", "#define_import_path bevy_pbr::shadows\nfn fetch_directional_shadow(id:u32,p:vec4<f32>,n:vec3<f32>,z:f32)->f32 {return 1.0;}"),
|
||||
("motion", "#define_import_path bevy_pbr::pbr_prepass_functions\nfn calculate_motion_vector(p:vec4<f32>,q:vec4<f32>)->vec2<f32> {return vec2<f32>(0.0);}"),
|
||||
("bindings",include_str!("retail_material_bindings.wgsl")),
|
||||
];
|
||||
for (path,source) in fixtures {
|
||||
if let Err(error) = composer.add_composable_module(ComposableModuleDescriptor {source,file_path:path,shader_defs:defs.clone(),..Default::default()}) {
|
||||
panic!("{}",error.emit_to_string(&composer));
|
||||
}
|
||||
}
|
||||
let source = if prepass {include_str!("retail_depth.wgsl")} else {include_str!("retail_world.wgsl")};
|
||||
let module = composer.make_naga_module(NagaModuleDescriptor {source,file_path:"retail",shader_defs:defs,..Default::default()})
|
||||
.unwrap_or_else(|e| panic!("{}",e.emit_to_string(&composer)));
|
||||
naga::valid::Validator::new(naga::valid::ValidationFlags::all(),naga::valid::Capabilities::all()).validate(&module).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn material_shaders_validate_with_bindless_and_fallback_bindings() {
|
||||
for bindless in [false,true] {
|
||||
validate(bindless,false,&[]);
|
||||
validate(bindless,true,&[]);
|
||||
validate(bindless,true,&["PREPASS_FRAGMENT","NORMAL_PREPASS","NORMAL_PREPASS_OR_DEFERRED_PREPASS","MOTION_VECTOR_PREPASS","UNCLIPPED_DEPTH_ORTHO_EMULATION"]);
|
||||
}
|
||||
}
|
||||
@@ -5,17 +5,25 @@
|
||||
#ifdef MOTION_VECTOR_PREPASS
|
||||
#import bevy_pbr::pbr_prepass_functions::calculate_motion_vector
|
||||
#endif
|
||||
struct WorldParams { mode: vec4<f32>, foliage_debug: vec4<f32>, surface: vec4<f32>, family: vec4<f32>, fog_ramp: vec4<f32>, fog_color: vec4<f32>, shadow_color: vec4<f32>, sun_direction: vec4<f32>, decal: vec4<f32>, water: array<vec4<f32>, 4> }
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(0) var<uniform> p: WorldParams;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(1) var diffuse: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(2) var diffuse_sampler: sampler;
|
||||
#import skate_retail::material_bindings as bindings
|
||||
#ifdef BINDLESS
|
||||
#import bevy_pbr::mesh_bindings::mesh
|
||||
#endif
|
||||
@fragment
|
||||
fn fragment(i: VertexOutput)
|
||||
#ifdef PREPASS_FRAGMENT
|
||||
-> FragmentOutput
|
||||
#endif
|
||||
{
|
||||
if textureSample(diffuse,diffuse_sampler,i.uv).a < p.mode.z { discard; }
|
||||
#ifdef BINDLESS
|
||||
let index = bindings::indices[mesh[i.instance_index].material_and_lightmap_bind_group_slot & 0xffffu];
|
||||
let alpha = textureSample(bindings::textures[index.diffuse],bindings::samplers[index.diffuse_sampler],i.uv).a;
|
||||
let cutoff = bindings::params[index.params].mode.z;
|
||||
#else
|
||||
let alpha = textureSample(bindings::diffuse,bindings::diffuse_sampler,i.uv).a;
|
||||
let cutoff = bindings::p.mode.z;
|
||||
#endif
|
||||
if alpha < cutoff { discard; }
|
||||
#ifdef PREPASS_FRAGMENT
|
||||
var out: FragmentOutput;
|
||||
#ifdef NORMAL_PREPASS
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#define_import_path skate_retail::material_bindings
|
||||
struct WorldParams {
|
||||
mode: vec4<f32>, foliage_debug: vec4<f32>, surface: vec4<f32>, family: vec4<f32>,
|
||||
fog_ramp: vec4<f32>, fog_color: vec4<f32>, shadow_color: vec4<f32>, sun_direction: vec4<f32>, decal: vec4<f32>, water: array<vec4<f32>, 4>,
|
||||
}
|
||||
struct FrameState { shadow: vec4<f32>, clock: vec4<f32>, pca: array<vec4<f32>, 7> }
|
||||
|
||||
#ifdef BINDLESS
|
||||
struct MaterialIndices {
|
||||
params: u32,
|
||||
diffuse: u32,
|
||||
diffuse_sampler: u32,
|
||||
lightmap: u32,
|
||||
lm_sampler: u32,
|
||||
normal_map: u32,
|
||||
normal_sampler: u32,
|
||||
detail_map: u32,
|
||||
detail_sampler: u32,
|
||||
macro_map: u32,
|
||||
macro_sampler: u32,
|
||||
decal_map: u32,
|
||||
decal_sampler: u32,
|
||||
specular_map: u32,
|
||||
specular_sampler: u32,
|
||||
environment_map: u32,
|
||||
frame_state: u32,
|
||||
}
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(0) var<storage> indices: array<MaterialIndices>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(1) var samplers: binding_array<sampler>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(5) var textures: binding_array<texture_2d<f32>>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(8) var cubes: binding_array<texture_cube<f32>>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(17) var<storage> params: array<WorldParams>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(18) var<storage> frames: binding_array<FrameState>;
|
||||
#else
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(0) var<uniform> p: WorldParams;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(1) var diffuse: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(2) var diffuse_sampler: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(3) var lightmap: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(4) var lm_sampler: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(5) var normal_map: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(6) var normal_sampler: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(7) var detail_map: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(8) var detail_sampler: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(9) var macro_map: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(10) var macro_sampler: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(11) var decal_map: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(12) var decal_sampler: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(13) var specular_map: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(14) var specular_sampler: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(15) var environment_map: texture_cube<f32>;
|
||||
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(16) var<storage, read> frame_state: FrameState;
|
||||
|
||||
#endif
|
||||
@@ -11,6 +11,9 @@ use bevy::{
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub(crate) struct RetailRenderPlugin;
|
||||
#[cfg(test)]
|
||||
#[path = "retail_bindless_tests.rs"]
|
||||
mod bindless_tests;
|
||||
// Use the same path derivation as embedded_asset!: alternate binary targets
|
||||
// have a different crate namespace even though they share these source files.
|
||||
fn retail_shader(path: &str) -> ShaderRef {
|
||||
@@ -18,7 +21,6 @@ fn retail_shader(path: &str) -> ShaderRef {
|
||||
}
|
||||
impl Plugin for RetailRenderPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
crate::skate_world::shadow_geometry::install(app);
|
||||
shadow::install(app);
|
||||
exposure::install(app);
|
||||
if std::env::var_os("SKATE_WEATHERING_COMPARE").is_some_and(|v| v == "1") {
|
||||
@@ -29,6 +31,7 @@ impl Plugin for RetailRenderPlugin {
|
||||
eprintln!("SKATE_FOLIAGE_DEBUG: solid cyan tree-wall cards, magenta other foliage; alpha rejection disabled for foliage only");
|
||||
}
|
||||
embedded_asset!(app, "retail_world.wgsl");
|
||||
bevy::shader::load_shader_library!(app, "retail_material_bindings.wgsl");
|
||||
embedded_asset!(app, "retail_tone.wgsl");
|
||||
embedded_asset!(app, "retail_depth.wgsl");
|
||||
embedded_asset!(app, "retail_sky.wgsl");
|
||||
@@ -111,8 +114,9 @@ pub(crate) struct WorldParams {
|
||||
|
||||
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
|
||||
#[bind_group_data(RetailKey)]
|
||||
#[data(0, WorldParams, binding_array(17))]
|
||||
#[bindless(limit(64))]
|
||||
pub(crate) struct RetailWorldMaterial {
|
||||
#[uniform(0)]
|
||||
pub params: WorldParams,
|
||||
#[texture(1)]
|
||||
#[sampler(2)]
|
||||
@@ -137,11 +141,14 @@ pub(crate) struct RetailWorldMaterial {
|
||||
pub specular: Option<Handle<Image>>,
|
||||
#[texture(15, dimension = "cube")]
|
||||
pub environment: Option<Handle<Image>>,
|
||||
#[storage(16, read_only)]
|
||||
#[storage(16, read_only, binding_array(18))]
|
||||
pub shadow_state: Handle<bevy::render::storage::ShaderStorageBuffer>,
|
||||
pub alpha: AlphaMode,
|
||||
pub two_sided: bool,
|
||||
}
|
||||
impl From<&RetailWorldMaterial> for WorldParams {
|
||||
fn from(material: &RetailWorldMaterial) -> Self { material.params.clone() }
|
||||
}
|
||||
/// Identity of the actual GPU inputs, independent of unused source metadata.
|
||||
#[derive(PartialEq, Eq, Hash)]
|
||||
pub(crate) struct WorldMaterialKey {
|
||||
|
||||
@@ -1,29 +1,11 @@
|
||||
#import bevy_pbr::{forward_io::VertexOutput, mesh_view_bindings as frame}
|
||||
#import bevy_pbr::shadows::fetch_directional_shadow
|
||||
|
||||
struct WorldParams {
|
||||
mode: vec4<f32>, foliage_debug: vec4<f32>, surface: vec4<f32>, family: vec4<f32>,
|
||||
fog_ramp: vec4<f32>, fog_color: vec4<f32>, shadow_color: vec4<f32>, sun_direction: vec4<f32>, decal: vec4<f32>, water: array<vec4<f32>, 4>,
|
||||
}
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(0) var<uniform> p: WorldParams;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(1) var diffuse: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(2) var diffuse_sampler: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(3) var lightmap: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(4) var lm_sampler: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(5) var normal_map: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(6) var normal_sampler: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(7) var detail_map: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(8) var detail_sampler: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(9) var macro_map: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(10) var macro_sampler: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(11) var decal_map: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(12) var decal_sampler: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(13) var specular_map: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(14) var specular_sampler: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(15) var environment_map: texture_cube<f32>;
|
||||
|
||||
struct FrameState { shadow: vec4<f32>, clock: vec4<f32>, pca: array<vec4<f32>, 7> }
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(16) var<storage, read> frame_state: FrameState;
|
||||
#import skate_retail::material_bindings::{WorldParams, FrameState}
|
||||
#import skate_retail::material_bindings as bindings
|
||||
#ifdef BINDLESS
|
||||
#import bevy_pbr::mesh_bindings::mesh
|
||||
#endif
|
||||
|
||||
// Textures and base UVs both have V flipped by the exporter. Scale in the
|
||||
// authored coordinate system, then return to the flipped texture rows.
|
||||
@@ -31,8 +13,25 @@ fn scaled_uv(uv: vec2<f32>, scale: f32) -> vec2<f32> {
|
||||
return vec2<f32>(uv.x*scale, 1.0-(1.0-uv.y)*scale);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fragment(i: VertexOutput) -> @location(0) vec4<f32> {
|
||||
fn shade(i: VertexOutput,
|
||||
p: WorldParams,
|
||||
frame_state: FrameState,
|
||||
diffuse: texture_2d<f32>,
|
||||
diffuse_sampler: sampler,
|
||||
lightmap: texture_2d<f32>,
|
||||
lm_sampler: sampler,
|
||||
normal_map: texture_2d<f32>,
|
||||
normal_sampler: sampler,
|
||||
detail_map: texture_2d<f32>,
|
||||
detail_sampler: sampler,
|
||||
macro_map: texture_2d<f32>,
|
||||
macro_sampler: sampler,
|
||||
decal_map: texture_2d<f32>,
|
||||
decal_sampler: sampler,
|
||||
specular_map: texture_2d<f32>,
|
||||
specular_sampler: sampler,
|
||||
environment_map: texture_cube<f32>
|
||||
) -> vec4<f32> {
|
||||
let fam = u32(p.mode.x);
|
||||
let flags = u32(p.mode.y);
|
||||
var diffuse_uv=i.uv;
|
||||
@@ -269,3 +268,47 @@ fn fragment(i: VertexOutput) -> @location(0) vec4<f32> {
|
||||
if a.a < p.mode.z { discard; }
|
||||
return vec4<f32>(xe,alpha);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fragment(i: VertexOutput) -> @location(0) vec4<f32> {
|
||||
#ifdef BINDLESS
|
||||
let index = bindings::indices[mesh[i.instance_index].material_and_lightmap_bind_group_slot & 0xffffu];
|
||||
return shade(i,
|
||||
bindings::params[index.params],
|
||||
bindings::frames[index.frame_state],
|
||||
bindings::textures[index.diffuse],
|
||||
bindings::samplers[index.diffuse_sampler],
|
||||
bindings::textures[index.lightmap],
|
||||
bindings::samplers[index.lm_sampler],
|
||||
bindings::textures[index.normal_map],
|
||||
bindings::samplers[index.normal_sampler],
|
||||
bindings::textures[index.detail_map],
|
||||
bindings::samplers[index.detail_sampler],
|
||||
bindings::textures[index.macro_map],
|
||||
bindings::samplers[index.macro_sampler],
|
||||
bindings::textures[index.decal_map],
|
||||
bindings::samplers[index.decal_sampler],
|
||||
bindings::textures[index.specular_map],
|
||||
bindings::samplers[index.specular_sampler],
|
||||
bindings::cubes[index.environment_map]);
|
||||
#else
|
||||
return shade(i,
|
||||
bindings::p,
|
||||
bindings::frame_state,
|
||||
bindings::diffuse,
|
||||
bindings::diffuse_sampler,
|
||||
bindings::lightmap,
|
||||
bindings::lm_sampler,
|
||||
bindings::normal_map,
|
||||
bindings::normal_sampler,
|
||||
bindings::detail_map,
|
||||
bindings::detail_sampler,
|
||||
bindings::macro_map,
|
||||
bindings::macro_sampler,
|
||||
bindings::decal_map,
|
||||
bindings::decal_sampler,
|
||||
bindings::specular_map,
|
||||
bindings::specular_sampler,
|
||||
bindings::environment_map);
|
||||
#endif
|
||||
}
|
||||
@@ -20,6 +20,7 @@ use skate_core::{
|
||||
use skate_data::skate_map::SkateMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "retail_shadow_geometry.rs"]
|
||||
pub(crate) mod shadow_geometry;
|
||||
|
||||
@@ -625,7 +626,6 @@ pub(crate) fn spawn(
|
||||
// lighting still needs separate geometry batches but can share a PBR material.
|
||||
let pbr_ids = self::material_ids(&map.materials, &texture_ids, false);
|
||||
let groups = consolidated_groups(map, &texture_ids, tuning, &mut texture);
|
||||
let mut shadow_geometry = shadow_geometry::ShadowGeometry::default();
|
||||
eprintln!(
|
||||
"SKATE_RENDER_BATCHES count={} triangles={}",
|
||||
groups.len(),
|
||||
@@ -635,10 +635,6 @@ pub(crate) fn spawn(
|
||||
vec![None; map.materials.len()];
|
||||
for RenderGroup { material: material_index, indices, retail } in groups {
|
||||
let m = &map.materials[material_index];
|
||||
let shadow_proxy = retail.as_ref().is_some_and(shadow_geometry::eligible);
|
||||
if shadow_proxy {
|
||||
shadow_geometry.add(&map.geometry, &indices, retail.as_ref().unwrap().two_sided);
|
||||
}
|
||||
// Reindex each batch, preserving authored normals and both UV sets.
|
||||
let mut remap = HashMap::new();
|
||||
let mut vertices = Vec::new();
|
||||
@@ -698,8 +694,7 @@ pub(crate) fn spawn(
|
||||
}
|
||||
if let Some(material) = retail {
|
||||
let material = retail_materials.add(material);
|
||||
let mut entity = commands.spawn((Name::new(m.name.clone()), Mesh3d(meshes.add(mesh)), MeshMaterial3d(material), Transform::default()));
|
||||
if shadow_proxy { entity.insert(bevy::light::NotShadowCaster); }
|
||||
commands.spawn((Name::new(m.name.clone()), Mesh3d(meshes.add(mesh)), MeshMaterial3d(material), Transform::default()));
|
||||
continue;
|
||||
}
|
||||
// Vertex colours above carry retail decal coordinates, never PBR tint.
|
||||
@@ -745,7 +740,6 @@ pub(crate) fn spawn(
|
||||
});
|
||||
}
|
||||
}
|
||||
shadow_geometry.spawn(&map.geometry, commands, meshes, materials);
|
||||
for light in &map.lights {
|
||||
let color = Color::linear_rgb(light.color[0], light.color[1], light.color[2]);
|
||||
let transform = Transform::from_translation(Vec3::from_array(light.position));
|
||||
@@ -850,7 +844,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opaque_shadow_batches_are_owned_and_retired_with_the_map() {
|
||||
fn retail_batches_are_owned_and_retired_with_the_map() {
|
||||
let map = retail_demo();
|
||||
let mut world = World::new();
|
||||
world.init_resource::<Assets<Mesh>>();
|
||||
@@ -861,8 +855,7 @@ mod tests {
|
||||
let mut scene = crate::map_render::PreparedScene::new(&world);
|
||||
scene.prepare(Some(&map), std::path::Path::new("unused"));
|
||||
scene.publish(&mut world);
|
||||
assert!(world.query_filtered::<Entity, With<bevy::light::NotShadowCaster>>().iter(&world).count() > 0);
|
||||
assert!(world.query::<&bevy::camera::visibility::RenderLayers>().iter(&world).any(|layers| layers.intersects(&bevy::camera::visibility::RenderLayers::layer(shadow_geometry::LAYER))));
|
||||
assert_eq!(world.query_filtered::<Entity, With<bevy::light::NotShadowCaster>>().iter(&world).count(), 0);
|
||||
crate::map_render::MapAssets::retire(&mut world);
|
||||
assert_eq!(world.query_filtered::<Entity, With<crate::map_render::MapEntity>>().iter(&world).count(), 0);
|
||||
assert_eq!(world.resource::<Assets<Mesh>>().len(), 0);
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# CPU material binding experiment
|
||||
|
||||
The user reports poor FPS while looking into University and good FPS looking out
|
||||
of the map, still CPU bound. The map being resident does not establish that every
|
||||
triangle is drawn. The captured CPU render work is the actionable bottleneck.
|
||||
|
||||
The v4 audit capture `university-audit-7802-28261.json` recorded 4,918 frames in
|
||||
20 seconds without dropped trace events. Mean command generation was 1.782 ms per
|
||||
frame; the main opaque encoder finish accounted for 0.995 ms, versus 0.349 ms in
|
||||
opaque draw traversal. The v5 shadow batch capture
|
||||
`university-shadow-batches-12344-4318.json` recorded 3,309 frames without dropped
|
||||
events. Main encoder finish remained 1.012 ms per frame. Shadow visibility fell
|
||||
from 0.466 to 0.349 ms, but that did not address the dominant main-pass cost.
|
||||
These are different workloads, not a controlled performance comparison.
|
||||
|
||||
A discarded CPU wall-occlusion prototype split at most 256 coarse batches and
|
||||
used conservative whole-AABB occlusion against real opaque wall triangles. Its
|
||||
static inward view culled only 55 of 3,512 candidates while costing about 1.15 ms.
|
||||
It was removed before shipping; adding that CPU cost was unjustified.
|
||||
|
||||
## Implementation
|
||||
|
||||
RetailWorldMaterial now uses Bevy's bindless material allocator, with a resource
|
||||
limit of 64 per slab. Materials share texture/sampler arrays and a packed parameter
|
||||
buffer. The existing shared frame/shadow state remains a buffer reference. This
|
||||
reduces distinct material binding groups and allows compatible meshes to share
|
||||
multidraw batches. Pipeline, mesh allocation and resource capacity still constrain
|
||||
batching; this is not a promise of a 64-fold draw reduction.
|
||||
|
||||
The main and depth/shadow shaders use the material slot stored in Bevy's mesh
|
||||
instance data. Both retain the original bindings when Bevy reports that bindless
|
||||
resources are unsupported. The common shader module is permanently loaded using
|
||||
Bevy's shader-library macro so custom imports resolve at runtime. The shading
|
||||
body, alpha rejection, geometry and texture content are preserved. No user
|
||||
graphics setting, native physics, input or simulation cadence changes.
|
||||
|
||||
The unsuccessful v5 extra shadow meshes and their light-layer adjustment are
|
||||
removed from the active renderer. Normal world meshes cast shadows again. The
|
||||
data-only shadow experiment remains test-only for historical reproducibility.
|
||||
|
||||
Trace configuration includes `retail_bindless`: true means the device supports
|
||||
the selected material-array path, false means fallback, null means device data
|
||||
was unavailable. This indicates capability selection, not measured draw counts.
|
||||
|
||||
## Validation and acceptance
|
||||
|
||||
GPU-free shader tests compose the actual material shaders and real Bevy vertex
|
||||
interfaces, using fixtures for unrelated lighting resources/functions. They
|
||||
validate six combinations: main, shadow depth and depth with normal/motion output,
|
||||
each with bindless enabled and disabled. A separate test checks the Rust-generated
|
||||
resource index table and buffer bindings against the shader contract.
|
||||
|
||||
The release shader tests passed (both tests, six shader variants). Eleven world
|
||||
adapter tests and two scene lifecycle tests passed; the offline map inventory
|
||||
test remains ignored in this ordinary regression run.
|
||||
|
||||
Runtime pipeline creation, appearance and FPS remain unverified until the user
|
||||
runs the private executable. The agent does not launch gameplay. Compare v4 and
|
||||
v6 from the same stationary inward-facing view with identical settings, then the
|
||||
same route. Inspect `retail_bindless`, main opaque encoder finish, total command
|
||||
generation and frame percentiles. Keep the earlier executables for rollback.
|
||||
@@ -1,5 +1,10 @@
|
||||
# University render batching experiment
|
||||
|
||||
**Superseded in the active renderer:** the user's v5 test did not improve the
|
||||
reported bottleneck. The v6 [CPU material binding experiment](cpu-material-binding-experiment.md)
|
||||
removes the extra shadow meshes and layer adjustment. The inventory below records
|
||||
the historical v5 candidate, not the current production spawn path.
|
||||
|
||||
The user authorized engine/content visual tradeoffs after the initial exact-output
|
||||
audit, while keeping resolution, MSAA, user settings and native gameplay timing
|
||||
unchanged. This first structural experiment targets redundant shadow submission;
|
||||
|
||||
Reference in new issue
Block a user