mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Add graphics settings and stabilize city-view render binding caches
This commit is contained in:
1 parent
83df8e62f4
commit
d31f81f95e
16 files changed
+788
-42
No files matched your search
@@ -1,6 +1,7 @@
|
||||
/target/
|
||||
/bin/
|
||||
/logs/
|
||||
/settings/
|
||||
/assets/private/
|
||||
/maps/private/
|
||||
/maps/*.skate
|
||||
|
||||
Generated
+1
@@ -4381,6 +4381,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"bevy",
|
||||
"half",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"skate-core",
|
||||
"skate-data",
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ try {
|
||||
if (-not (Test-Path -LiteralPath $executable)) { throw 'Game is not built. Run BUILD.bat first.' }
|
||||
New-Item -ItemType Directory -Path (Join-Path $PSScriptRoot 'logs') -Force | Out-Null
|
||||
$log = Join-Path $PSScriptRoot ('logs/game-' + (Get-Date -Format 'yyyyMMdd-HHmmss') + '.log')
|
||||
Write-Host 'Starting imported skater project (Easy). Use your XInput controller; Esc exits.'
|
||||
Write-Host 'Starting imported skater project (Easy). Use your XInput controller; Esc opens graphics/pause menu.'
|
||||
Write-Host "Log: $log"
|
||||
$errorLog = [System.IO.Path]::ChangeExtension($log, 'stderr.log')
|
||||
$assetArgument = '"' + (Join-Path $PSScriptRoot 'assets') + '"'
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
# Imported skater project
|
||||
|
||||
Double-click **PLAY.bat** to launch. Use an XInput-compatible controller;
|
||||
**Esc** exits. Startup selects the supplied **Easy** profile.
|
||||
**Esc** opens the pause/graphics menu. Startup selects the supplied **Easy** profile.
|
||||
|
||||
The menu supports window resolution (720p through 4K), internal render scale
|
||||
(25–100%), supported MSAA levels (Off/2×/4×/8×), and an FPS cap (30–240 or
|
||||
Unlimited). Click a row to cycle it, or use arrow keys and Enter. Changes apply
|
||||
immediately and save to `settings/graphics.json`. Escape or Resume returns to
|
||||
gameplay; Quit game exits. The menu stays at the window's native resolution.
|
||||
|
||||
The standing/rolling **360 flip** now uses the supplied gesture files and
|
||||
ActionGraph/MotionGraph route, including right-stick crouch preparation.
|
||||
|
||||
@@ -10,3 +10,4 @@ skate-core.workspace = true
|
||||
skate-data.workspace = true
|
||||
half = "=2.7.1"
|
||||
serde_json.workspace = true
|
||||
serde.workspace = true
|
||||
@@ -94,6 +94,8 @@ pub(crate) fn build(
|
||||
animation::AnimationPlugin,
|
||||
world::WorldPlugin,
|
||||
camera::CameraPlugin,
|
||||
crate::graphics_menu::GraphicsMenuPlugin,
|
||||
crate::render_capacity::RenderCapacityPlugin,
|
||||
verification::VerificationPlugin,
|
||||
crate::performance::PerformancePlugin,
|
||||
));
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
//! Native-resolution pause UI over a separately scaled 3D render target.
|
||||
use bevy::{
|
||||
camera::RenderTarget,
|
||||
image::ImageSampler,
|
||||
prelude::*,
|
||||
render::{
|
||||
render_resource::{Extent3d, TextureFormat},
|
||||
renderer::RenderAdapter,
|
||||
},
|
||||
window::{PresentMode, PrimaryWindow},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
const RESOLUTIONS: &[(u32, u32)] = &[
|
||||
(1280, 720),
|
||||
(1280, 800),
|
||||
(1600, 900),
|
||||
(1920, 1080),
|
||||
(1920, 1200),
|
||||
(2560, 1440),
|
||||
(2560, 1600),
|
||||
(3840, 2160),
|
||||
];
|
||||
const SCALES: &[u32] = &[25, 50, 67, 75, 85, 100];
|
||||
const LIMITS: &[u32] = &[0, 30, 60, 90, 120, 144, 165, 240];
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
struct GraphicsSettings {
|
||||
width: u32,
|
||||
height: u32,
|
||||
scale: u32,
|
||||
samples: u32,
|
||||
fps: u32,
|
||||
}
|
||||
impl Default for GraphicsSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
width: 1280,
|
||||
height: 800,
|
||||
scale: 100,
|
||||
samples: 4,
|
||||
fps: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl GraphicsSettings {
|
||||
fn validated(mut self) -> Self {
|
||||
if !RESOLUTIONS.contains(&(self.width, self.height)) {
|
||||
(self.width, self.height) = (1280, 800);
|
||||
}
|
||||
if !SCALES.contains(&self.scale) {
|
||||
self.scale = 100;
|
||||
}
|
||||
if ![1, 2, 4, 8].contains(&self.samples) {
|
||||
self.samples = 4;
|
||||
}
|
||||
if !LIMITS.contains(&self.fps) {
|
||||
self.fps = 0;
|
||||
}
|
||||
self
|
||||
}
|
||||
fn internal_size(&self, window: UVec2) -> UVec2 {
|
||||
(window * self.scale / 100).max(UVec2::ONE)
|
||||
}
|
||||
}
|
||||
#[derive(Resource)]
|
||||
pub(crate) struct Menu {
|
||||
open: bool,
|
||||
selected: usize,
|
||||
settings: GraphicsSettings,
|
||||
path: PathBuf,
|
||||
supported_msaa: Vec<u32>,
|
||||
status: String,
|
||||
}
|
||||
pub(crate) fn gameplay_active(menu: Option<Res<Menu>>) -> bool {
|
||||
menu.is_none_or(|m| !m.open)
|
||||
}
|
||||
#[derive(Resource)]
|
||||
struct SceneTarget(Handle<Image>);
|
||||
#[derive(Resource)]
|
||||
struct FramePacer(Instant);
|
||||
#[derive(Component)]
|
||||
struct MenuRoot;
|
||||
#[derive(Component)]
|
||||
struct MenuRow(usize);
|
||||
#[derive(Component)]
|
||||
struct MenuLabel(usize);
|
||||
#[derive(Component)]
|
||||
struct StatusLabel;
|
||||
|
||||
pub(crate) struct GraphicsMenuPlugin;
|
||||
impl Plugin for GraphicsMenuPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.insert_resource(FramePacer(Instant::now()))
|
||||
.add_systems(PostStartup, setup)
|
||||
.add_systems(Update, (interact, apply, labels).chain())
|
||||
.add_systems(Last, pace);
|
||||
}
|
||||
}
|
||||
fn setup(
|
||||
mut commands: Commands,
|
||||
config: Res<crate::config::Config>,
|
||||
mut images: ResMut<Assets<Image>>,
|
||||
mut window: Single<&mut Window, With<PrimaryWindow>>,
|
||||
cameras: Query<Entity, With<Camera3d>>,
|
||||
adapter: Res<RenderAdapter>,
|
||||
) {
|
||||
let path = config
|
||||
.asset_root
|
||||
.parent()
|
||||
.unwrap_or(&config.asset_root)
|
||||
.join("settings/graphics.json");
|
||||
let settings = match std::fs::read(&path) {
|
||||
Ok(bytes) => serde_json::from_slice::<GraphicsSettings>(&bytes).unwrap_or_else(|e| {
|
||||
warn!("Graphics settings: {e}");
|
||||
GraphicsSettings::default()
|
||||
}),
|
||||
Err(_) => GraphicsSettings::default(),
|
||||
}
|
||||
.validated();
|
||||
let supported_msaa: Vec<_> = [1, 2, 4, 8]
|
||||
.into_iter()
|
||||
.filter(|&samples| {
|
||||
[
|
||||
TextureFormat::Rgba16Float,
|
||||
TextureFormat::Rgba8UnormSrgb,
|
||||
TextureFormat::Depth32Float,
|
||||
]
|
||||
.into_iter()
|
||||
.all(|format| {
|
||||
adapter
|
||||
.get_texture_format_features(format)
|
||||
.flags
|
||||
.sample_count_supported(samples)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let mut settings = settings;
|
||||
if !supported_msaa.contains(&settings.samples) {
|
||||
settings.samples = 1;
|
||||
}
|
||||
window
|
||||
.resolution
|
||||
.set_physical_resolution(settings.width, settings.height);
|
||||
window.present_mode = PresentMode::AutoNoVsync;
|
||||
let size = settings.internal_size(window.physical_size());
|
||||
let mut image = Image::new_target_texture(size.x, size.y, TextureFormat::Rgba8UnormSrgb, None);
|
||||
image.sampler = ImageSampler::linear();
|
||||
let target = images.add(image);
|
||||
for camera in &cameras {
|
||||
commands.entity(camera).insert((
|
||||
RenderTarget::Image(target.clone().into()),
|
||||
msaa(settings.samples),
|
||||
));
|
||||
// Render the world before the presentation camera consumes its image.
|
||||
commands.entity(camera).insert(Camera {
|
||||
order: -1,
|
||||
..default()
|
||||
});
|
||||
}
|
||||
let output = commands
|
||||
.spawn((Camera2d, Msaa::Off, IsDefaultUiCamera))
|
||||
.id();
|
||||
commands.spawn((
|
||||
Node {
|
||||
width: percent(100),
|
||||
height: percent(100),
|
||||
position_type: PositionType::Absolute,
|
||||
..default()
|
||||
},
|
||||
ImageNode::new(target.clone()),
|
||||
UiTargetCamera(output),
|
||||
));
|
||||
commands.spawn((MenuRoot, UiTargetCamera(output), GlobalZIndex(10), Node {
|
||||
display: Display::None, width:percent(100), height:percent(100), align_items:AlignItems::Center,
|
||||
justify_content:JustifyContent::Center, position_type:PositionType::Absolute, ..default()
|
||||
}, BackgroundColor(Color::srgba(0.015,0.025,0.04,0.88)))).with_children(|root| {
|
||||
root.spawn((Node { width:px(560),max_width:percent(95),padding:UiRect::all(px(28)),flex_direction:FlexDirection::Column,row_gap:px(10),border_radius:BorderRadius::all(px(12)),..default() },
|
||||
BackgroundColor(Color::srgb(0.035,0.055,0.08)))).with_children(|panel| {
|
||||
panel.spawn((Text::new("PAUSED"),TextFont {font_size:32.,..default()},TextColor(Color::WHITE)));
|
||||
panel.spawn((Text::new("GRAPHICS"),TextFont {font_size:16.,..default()},TextColor(Color::srgb(0.4,0.85,0.85))));
|
||||
for i in 0..6 {
|
||||
panel.spawn((Button, MenuRow(i), Node {width:percent(100),min_height:px(48),padding:UiRect::all(px(12)),align_items:AlignItems::Center,border_radius:BorderRadius::all(px(5)),..default()},
|
||||
BackgroundColor(Color::srgb(0.08,0.11,0.15)))).with_children(|row| {
|
||||
row.spawn((MenuLabel(i),Text::new(""),TextFont {font_size:20.,..default()},TextColor(Color::WHITE)));
|
||||
});
|
||||
}
|
||||
panel.spawn((StatusLabel,Text::new(""),TextFont {font_size:15.,..default()},TextColor(Color::srgb(0.65,0.75,0.8))));
|
||||
panel.spawn((Text::new("Click to cycle | Up/Down select | Left/Right change\nEsc resume | Changes save automatically"),TextFont {font_size:14.,..default()},TextColor(Color::srgb(0.65,0.75,0.8))));
|
||||
});
|
||||
});
|
||||
commands.insert_resource(SceneTarget(target));
|
||||
commands.insert_resource(Menu {
|
||||
open: false,
|
||||
selected: 0,
|
||||
settings,
|
||||
path,
|
||||
supported_msaa,
|
||||
status: String::new(),
|
||||
});
|
||||
}
|
||||
fn msaa(samples: u32) -> Msaa {
|
||||
match samples {
|
||||
2 => Msaa::Sample2,
|
||||
4 => Msaa::Sample4,
|
||||
8 => Msaa::Sample8,
|
||||
_ => Msaa::Off,
|
||||
}
|
||||
}
|
||||
fn cycle<T: PartialEq + Copy>(values: &[T], value: T, direction: i32) -> T {
|
||||
let index = values.iter().position(|x| *x == value).unwrap_or(0) as i32;
|
||||
values[(index + direction).rem_euclid(values.len() as i32) as usize]
|
||||
}
|
||||
fn interact(
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
mut menu: ResMut<Menu>,
|
||||
mut time: ResMut<Time<Virtual>>,
|
||||
buttons: Query<(&Interaction, &MenuRow), Changed<Interaction>>,
|
||||
mut exit: MessageWriter<AppExit>,
|
||||
) {
|
||||
if keys.just_pressed(KeyCode::Escape) {
|
||||
menu.open = !menu.open;
|
||||
}
|
||||
let mut action = None;
|
||||
if menu.open {
|
||||
if keys.just_pressed(KeyCode::ArrowUp) {
|
||||
menu.selected = (menu.selected + 5) % 6;
|
||||
}
|
||||
if keys.just_pressed(KeyCode::ArrowDown) {
|
||||
menu.selected = (menu.selected + 1) % 6;
|
||||
}
|
||||
if keys.just_pressed(KeyCode::ArrowLeft) {
|
||||
action = Some((menu.selected, -1));
|
||||
}
|
||||
if keys.just_pressed(KeyCode::ArrowRight) || keys.just_pressed(KeyCode::Enter) {
|
||||
action = Some((menu.selected, 1));
|
||||
}
|
||||
for (interaction, row) in &buttons {
|
||||
if *interaction == Interaction::Pressed {
|
||||
menu.selected = row.0;
|
||||
action = Some((row.0, 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((row, direction)) = action {
|
||||
match row {
|
||||
0 => {
|
||||
let size = cycle(
|
||||
RESOLUTIONS,
|
||||
(menu.settings.width, menu.settings.height),
|
||||
direction,
|
||||
);
|
||||
(menu.settings.width, menu.settings.height) = size;
|
||||
}
|
||||
1 => menu.settings.scale = cycle(SCALES, menu.settings.scale, direction),
|
||||
2 => {
|
||||
menu.settings.samples =
|
||||
cycle(&menu.supported_msaa, menu.settings.samples, direction)
|
||||
}
|
||||
3 => menu.settings.fps = cycle(LIMITS, menu.settings.fps, direction),
|
||||
4 => menu.open = false,
|
||||
5 => {
|
||||
exit.write(AppExit::Success);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if row < 4 {
|
||||
let save = (|| -> Result<(), String> {
|
||||
std::fs::create_dir_all(menu.path.parent().unwrap()).map_err(|e| e.to_string())?;
|
||||
std::fs::write(
|
||||
&menu.path,
|
||||
serde_json::to_vec_pretty(&menu.settings).map_err(|e| e.to_string())?,
|
||||
)
|
||||
.map_err(|e| e.to_string())
|
||||
})();
|
||||
menu.status = match save {
|
||||
Ok(()) => "Saved".into(),
|
||||
Err(e) => format!("Could not save: {e}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
if menu.open {
|
||||
time.pause();
|
||||
} else {
|
||||
time.unpause();
|
||||
}
|
||||
}
|
||||
fn apply(
|
||||
menu: Res<Menu>,
|
||||
mut window: Single<&mut Window, With<PrimaryWindow>>,
|
||||
target: Res<SceneTarget>,
|
||||
mut images: ResMut<Assets<Image>>,
|
||||
mut cameras: Query<&mut Msaa, With<Camera3d>>,
|
||||
mut previous: Local<Option<GraphicsSettings>>,
|
||||
) {
|
||||
if previous
|
||||
.as_ref()
|
||||
.is_none_or(|p| p.width != menu.settings.width || p.height != menu.settings.height)
|
||||
{
|
||||
window
|
||||
.resolution
|
||||
.set_physical_resolution(menu.settings.width, menu.settings.height);
|
||||
}
|
||||
if previous
|
||||
.as_ref()
|
||||
.is_none_or(|p| p.samples != menu.settings.samples)
|
||||
{
|
||||
for mut samples in &mut cameras {
|
||||
*samples = msaa(menu.settings.samples);
|
||||
}
|
||||
}
|
||||
let size = menu.settings.internal_size(window.physical_size());
|
||||
if let Some(image) = images.get(&target.0) {
|
||||
if image.size() != size {
|
||||
images.get_mut(&target.0).unwrap().resize(Extent3d {
|
||||
width: size.x,
|
||||
height: size.y,
|
||||
depth_or_array_layers: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
*previous = Some(menu.settings.clone());
|
||||
}
|
||||
fn labels(
|
||||
menu: Res<Menu>,
|
||||
window: Single<&Window, With<PrimaryWindow>>,
|
||||
mut root: Single<&mut Node, With<MenuRoot>>,
|
||||
mut labels: Query<(&MenuLabel, &mut Text), Without<StatusLabel>>,
|
||||
mut status: Single<&mut Text, With<StatusLabel>>,
|
||||
mut buttons: Query<(&MenuRow, &Interaction, &mut BackgroundColor)>,
|
||||
) {
|
||||
root.display = if menu.open {
|
||||
Display::Flex
|
||||
} else {
|
||||
Display::None
|
||||
};
|
||||
if !menu.open {
|
||||
return;
|
||||
}
|
||||
let s = &menu.settings;
|
||||
let size = s.internal_size(window.physical_size());
|
||||
for (label, mut text) in &mut labels {
|
||||
**text = match label.0 {
|
||||
0 => format!("Resolution {} x {}", s.width, s.height),
|
||||
1 => format!(
|
||||
"Internal resolution {}% ({} x {})",
|
||||
s.scale, size.x, size.y
|
||||
),
|
||||
2 => format!(
|
||||
"MSAA {}",
|
||||
if s.samples == 1 {
|
||||
"Off".into()
|
||||
} else {
|
||||
format!("{}x", s.samples)
|
||||
}
|
||||
),
|
||||
3 => format!(
|
||||
"FPS limit {}",
|
||||
if s.fps == 0 {
|
||||
"Unlimited".into()
|
||||
} else {
|
||||
s.fps.to_string()
|
||||
}
|
||||
),
|
||||
4 => "Resume".into(),
|
||||
_ => "Quit game".into(),
|
||||
};
|
||||
}
|
||||
***status = menu.status.clone();
|
||||
for (row, interaction, mut color) in &mut buttons {
|
||||
color.0 = if row.0 == menu.selected || *interaction == Interaction::Hovered {
|
||||
Color::srgb(0.10, 0.30, 0.34)
|
||||
} else {
|
||||
Color::srgb(0.08, 0.11, 0.15)
|
||||
};
|
||||
}
|
||||
}
|
||||
fn pace(menu: Option<Res<Menu>>, mut pacer: ResMut<FramePacer>) {
|
||||
let Some(menu) = menu else {
|
||||
return;
|
||||
};
|
||||
if menu.settings.fps > 0 {
|
||||
let period = Duration::from_secs_f64(1. / f64::from(menu.settings.fps));
|
||||
if let Some(wait) = period.checked_sub(pacer.0.elapsed()) {
|
||||
std::thread::sleep(wait);
|
||||
}
|
||||
}
|
||||
pacer.0 = Instant::now();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn invalid_saved_values_fall_back() {
|
||||
let settings: GraphicsSettings =
|
||||
serde_json::from_str(r#"{"width":0,"height":999999,"scale":0,"samples":3,"fps":1}"#)
|
||||
.unwrap();
|
||||
assert_eq!(settings.validated(), GraphicsSettings::default());
|
||||
}
|
||||
#[test]
|
||||
fn scaled_target_and_cycle_boundaries() {
|
||||
let s = GraphicsSettings {
|
||||
scale: 50,
|
||||
..default()
|
||||
};
|
||||
assert_eq!(
|
||||
s.internal_size(UVec2::new(1920, 1080)),
|
||||
UVec2::new(960, 540)
|
||||
);
|
||||
assert_eq!(s.internal_size(UVec2::ZERO), UVec2::ONE);
|
||||
assert_eq!(cycle(LIMITS, 0, -1), 240);
|
||||
assert_eq!(cycle(LIMITS, 240, 1), 0);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ pub(crate) struct InputPlugin;
|
||||
impl Plugin for InputPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<ControllerInput>()
|
||||
.add_systems(PreUpdate, (poll_controllers, exit_requested))
|
||||
.add_systems(PreUpdate, poll_controllers.run_if(crate::graphics_menu::gameplay_active))
|
||||
.add_systems(FixedUpdate, publish_actions.in_set(SimulationSet::Input));
|
||||
}
|
||||
}
|
||||
@@ -38,8 +38,3 @@ fn poll_controllers(mut input: ResMut<ControllerInput>) {
|
||||
fn publish_actions(mut input: ResMut<ControllerInput>) {
|
||||
input.publish_actions();
|
||||
}
|
||||
fn exit_requested(keys: Res<ButtonInput<KeyCode>>, mut exit: MessageWriter<AppExit>) {
|
||||
if keys.just_pressed(KeyCode::Escape) {
|
||||
exit.write(AppExit::Success);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@ mod physics;
|
||||
mod skater_animation;
|
||||
mod verification;
|
||||
mod performance;
|
||||
mod graphics_menu;
|
||||
mod render_capacity;
|
||||
mod world;
|
||||
mod skate_world;
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
//! Keep instance-buffer identities stable as views discover more geometry.
|
||||
use bevy::{
|
||||
pbr::{MeshInputUniform, MeshPipeline, MeshUniform},
|
||||
prelude::*,
|
||||
render::{
|
||||
Render, RenderApp, RenderSystems,
|
||||
batching::gpu_preprocessing::{BatchedInstanceBuffers, write_batched_instance_buffers},
|
||||
renderer::RenderDevice,
|
||||
},
|
||||
};
|
||||
|
||||
pub(crate) struct RenderCapacityPlugin;
|
||||
impl Plugin for RenderCapacityPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
if let Some(render) = app.get_sub_app_mut(RenderApp) {
|
||||
render.add_systems(
|
||||
Render,
|
||||
reserve_instances
|
||||
.in_set(RenderSystems::PrepareResourcesFlush)
|
||||
.before(write_batched_instance_buffers::<MeshPipeline>),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn capacity(required: usize, limit: usize) -> usize {
|
||||
if required == 0 {
|
||||
return 0;
|
||||
}
|
||||
required
|
||||
.checked_next_power_of_two()
|
||||
.unwrap_or(required)
|
||||
.min(limit)
|
||||
.max(required)
|
||||
}
|
||||
|
||||
fn reserve_instances(
|
||||
device: Res<RenderDevice>,
|
||||
buffers: Option<ResMut<BatchedInstanceBuffers<MeshUniform, MeshInputUniform>>>,
|
||||
) {
|
||||
let Some(mut buffers) = buffers else {
|
||||
return;
|
||||
};
|
||||
let limits = device.limits();
|
||||
let maximum = limits
|
||||
.max_buffer_size
|
||||
.min(u64::from(limits.max_storage_buffer_binding_size))
|
||||
/ std::mem::size_of::<MeshUniform>() as u64;
|
||||
for phase in buffers.phase_instance_buffers.values_mut() {
|
||||
let required = phase.data_buffer.len();
|
||||
if required != 0 {
|
||||
// Bevy's subsequent write_buffer preserves the logical count and
|
||||
// initializes only the actual instances through GPU preprocessing.
|
||||
phase
|
||||
.data_buffer
|
||||
.reserve(capacity(required, maximum as usize), &device);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn growth_is_bounded_and_never_smaller_than_required() {
|
||||
assert_eq!(capacity(0, 100), 0);
|
||||
assert_eq!(capacity(33, 100), 64);
|
||||
assert_eq!(capacity(64, 100), 64);
|
||||
assert_eq!(capacity(65, 100), 100);
|
||||
// Leave unsupported requests to Bevy's validation; never truncate data.
|
||||
assert_eq!(capacity(101, 100), 101);
|
||||
for n in 1..10000 {
|
||||
assert!(capacity(n, 10000) >= n);
|
||||
assert!(capacity(n, 10000) <= 10000);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -365,6 +365,14 @@ fn render_texture_ids(textures: &[skate_data::skate_map::Texture]) -> Vec<u32> {
|
||||
fn render_material_ids(
|
||||
materials: &[skate_data::skate_map::Material],
|
||||
texture_ids: &[u32],
|
||||
) -> Vec<usize> {
|
||||
material_ids(materials, texture_ids, true)
|
||||
}
|
||||
|
||||
fn material_ids(
|
||||
materials: &[skate_data::skate_map::Material],
|
||||
texture_ids: &[u32],
|
||||
include_lightmap: bool,
|
||||
) -> Vec<usize> {
|
||||
let mut unique = HashMap::new();
|
||||
materials
|
||||
@@ -376,7 +384,7 @@ fn render_material_ids(
|
||||
let textures = m.textures.map(|id| texture_ids[id as usize]);
|
||||
let key = [
|
||||
textures[0],
|
||||
textures[1],
|
||||
if include_lightmap { textures[1] } else { 0 },
|
||||
textures[2],
|
||||
textures[3],
|
||||
textures[4],
|
||||
@@ -477,6 +485,9 @@ pub(crate) fn spawn(
|
||||
)
|
||||
};
|
||||
let material_ids = render_material_ids(&map.materials, &texture_ids);
|
||||
// Lightmaps belong to mesh entities, not StandardMaterial. Distinct baked
|
||||
// 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 = render_groups(&map.geometry, &material_ids);
|
||||
eprintln!(
|
||||
"SKATE_RENDER_BATCHES count={} triangles={}",
|
||||
@@ -540,7 +551,7 @@ pub(crate) fn spawn(
|
||||
warn!("SKATE material {} tangent generation: {error}", m.name);
|
||||
}
|
||||
}
|
||||
let material = material_handles[material_index]
|
||||
let material = material_handles[pbr_ids[material_index]]
|
||||
.get_or_insert_with(|| {
|
||||
let orm = texture(m.textures[3], 2);
|
||||
materials.add(StandardMaterial {
|
||||
@@ -683,6 +694,9 @@ mod tests {
|
||||
map.materials[1].roughness = map.materials[0].roughness;
|
||||
map.materials[1].textures[1] = 0;
|
||||
assert_eq!(render_material_ids(&map.materials, &texture_ids), [0, 1]);
|
||||
assert_eq!(material_ids(&map.materials, &texture_ids, false), [0, 0]);
|
||||
map.materials[1].indirect_strength += 1.;
|
||||
assert_eq!(material_ids(&map.materials, &texture_ids, false), [0, 1]);
|
||||
}
|
||||
#[test]
|
||||
fn texture_sharing_preserves_pixels_dimensions_and_color_space() {
|
||||
|
||||
@@ -97,3 +97,48 @@ baseline. Exploration can still encounter heavier views and loading/shader spike
|
||||
Validation: GPU cache regression test passed, game suite passed (43 passed,
|
||||
21 existing ignored), and the staged executable completed a normal city startup
|
||||
capture with benchmark settings disabled.
|
||||
|
||||
## City-facing dips at 1440p / 8x MSAA
|
||||
|
||||
The next pass uses the user's saved 2560 x 1440, 100% internal resolution,
|
||||
8x MSAA, unlimited FPS settings and the same rotating-camera benchmark.
|
||||
Do not compare these results directly with the earlier stationary 1280 x 800 run.
|
||||
|
||||
| Metric | Before | After |
|
||||
| --- | ---: | ---: |
|
||||
| Average FPS | 84.9 | 119.1 |
|
||||
| Frame interval p95 | 28.9 ms | 23.5 ms |
|
||||
| Render preparation per frame | 5.42 ms | 1.49 ms |
|
||||
|
||||
Reports: `logs/city-baseline.json` and `logs/city-double.json`. Temporary direct
|
||||
instrumentation measured roughly 3–4 ms in mesh binding preparation before the
|
||||
final fix and about 0.05 ms after warmup. GPU telemetry during the optimized run
|
||||
sampled roughly 24–39% usage. Heavy views and frame spikes remain.
|
||||
The final clean build, with temporary tracing and GPU telemetry polling removed,
|
||||
averaged 145.0 FPS with an 18.9 ms p95 frame interval (`logs/city-final.json`).
|
||||
The 119–145 FPS range illustrates run/instrumentation variation; it is not a
|
||||
guaranteed minimum while exploring the whole map.
|
||||
|
||||
Bevy's `collect_buffers_for_phase` exchanges two buffer allocations every frame.
|
||||
A single-table cache therefore misses every frame even with static resource
|
||||
contents. The patch now retains both phase tables, checks resource identities
|
||||
and lightmap revisions, and shares unchanged tables without visiting every slab.
|
||||
The preceding one-table trial did not materially improve the sweep and is not
|
||||
the implementation shipped here.
|
||||
|
||||
The game also reserves geometrically growing instance-buffer capacity before
|
||||
Bevy writes it, bounded by device limits. This reduces reallocations when visible
|
||||
counts increase; it does not change logical counts or dispatch extra instances.
|
||||
Mesh morph-target lookup is refreshed on GPU asset changes, and identical PBR
|
||||
materials can share handles while each mesh retains its own baked lightmap.
|
||||
|
||||
No triangles, collision detail, lighting, or draw distance were removed. LODs
|
||||
remain a possible next step for distant geometry, particularly hierarchical LODs
|
||||
that also reduce the number of draws. They do not address the buffer-cache miss
|
||||
fixed here. GPU occlusion culling is also relevant to a dense city, but the current
|
||||
engine's version is experimental: [Bevy's migration notes](https://bevy.org/learn/migration-guides/0-18-to-0-19/#occlusion-culling-is-no-longer-experimental)
|
||||
describe correctness fixes in 0.19. It was not enabled as an unverified workaround.
|
||||
|
||||
Validation: the explicit GPU regression test and game suite (46 passed,
|
||||
21 existing ignored) passed. The staged build completed the normal San Vanelona
|
||||
startup capture at the user's saved settings, with benchmark overrides disabled.
|
||||
Vendored
+11
-1
@@ -19,6 +19,15 @@ Local changes are restricted to:
|
||||
- `src/render/mesh_bindings.rs`: reuse or create the binding through that cache.
|
||||
The lightmap slab parameter is now mutable.
|
||||
- `src/lightmap/cache_tests.rs`: explicit GPU regression test.
|
||||
- `src/render/mesh.rs`: retain complete lookup tables for both alternating
|
||||
instance buffers per phase. Keys include buffer identity, offset, size, layout,
|
||||
and the lightmap binding revision. A slab creation, texture upload, or removal
|
||||
changes that revision. Tables are shared by `Arc`, so a cache hit avoids
|
||||
rebuilding and cloning thousands of individual entries. Keep only two tables
|
||||
per phase, and discard caches for phases that disappear.
|
||||
- `src/render/mesh.rs`: cache the IDs of meshes with morph targets when
|
||||
`RenderAssets<RenderMesh>` changes. Each phase still rebuilds its dynamic morph
|
||||
bindings from the current GPU resources, but skips scanning static city meshes.
|
||||
|
||||
Writing new instance data into an existing buffer does not require rebuilding a
|
||||
bind group. Changing the buffer, bound range, layout, or texture does. The patch
|
||||
@@ -33,7 +42,8 @@ cargo test -p bevy_pbr --lib cache_tracks_binding_identity_and_texture_replaceme
|
||||
|
||||
It covers cloned versus replaced buffers, changed binding ranges/layouts, writes
|
||||
to an existing buffer, texture insertion/removal, bounded eviction, and actual
|
||||
bind-group construction/reuse. Regular game tests and a city screenshot/benchmark
|
||||
bind-group construction/reuse, alternating-buffer table reuse, table revisions,
|
||||
and morph-target additions/replacements/removals. Regular game tests and a city screenshot/benchmark
|
||||
also exercise the patched renderer.
|
||||
|
||||
Upstream references reviewed September 6, 2026:
|
||||
|
||||
+47
@@ -58,6 +58,22 @@ fn cache_tracks_binding_identity_and_texture_replacement() {
|
||||
let layout = device.create_bind_group_layout(None, &[]);
|
||||
let different_layout = device.create_bind_group_layout(None, &[]);
|
||||
let group = device.create_bind_group(None, &layout, &[]);
|
||||
let mut table = crate::render::mesh::PhaseLightmapCache::default();
|
||||
table.remember(&binding(&a, 0, 256), layout.id(), 7);
|
||||
assert!(table.matches(&binding(&a.clone(), 0, 256), layout.id(), 7));
|
||||
assert!(!table.matches(&binding(&a, 0, 256), layout.id(), 8));
|
||||
assert!(!table.matches(&binding(&b, 0, 256), layout.id(), 7));
|
||||
assert!(!table.matches(&binding(&a, 256, 256), layout.id(), 7));
|
||||
assert!(!table.matches(&binding(&a, 0, 512), layout.id(), 7));
|
||||
assert!(!table.matches(&binding(&a, 0, 256), different_layout.id(), 7));
|
||||
table.remember(&binding(&b, 0, 256), layout.id(), 7);
|
||||
// Bevy exchanges its per-phase buffers every frame. Both allocations must
|
||||
// remain reusable, rather than evicting one another on every camera turn.
|
||||
for _ in 0..10 {
|
||||
assert!(table.select(&binding(&a, 0, 256), layout.id(), 7));
|
||||
assert!(table.select(&binding(&b, 0, 256), layout.id(), 7));
|
||||
}
|
||||
assert!(!table.select(&binding(&a, 0, 256), layout.id(), 8));
|
||||
let extent = Extent3d {
|
||||
width: 1,
|
||||
height: 1,
|
||||
@@ -91,6 +107,37 @@ fn cache_tracks_binding_identity_and_texture_replacement() {
|
||||
cube_array: image.clone(),
|
||||
d3: image.clone(),
|
||||
};
|
||||
// Adding/removing morph targets must refresh the compact mesh list.
|
||||
let source = bevy_mesh::Mesh::new(
|
||||
PrimitiveTopology::TriangleList,
|
||||
bevy_asset::RenderAssetUsages::default(),
|
||||
);
|
||||
let mut layouts = bevy_mesh::MeshVertexBufferLayouts::default();
|
||||
let mesh_layout = source.get_mesh_vertex_buffer_layout(&mut layouts);
|
||||
let mesh = |morph| bevy_render::mesh::RenderMesh {
|
||||
vertex_count: 0,
|
||||
morph_targets: morph,
|
||||
buffer_info: bevy_render::mesh::RenderMeshBufferInfo::NonIndexed,
|
||||
key_bits: bevy_mesh::BaseMeshPipelineKey::empty(),
|
||||
layout: mesh_layout.clone(),
|
||||
};
|
||||
let static_id = AssetId::Uuid {
|
||||
uuid: bevy_asset::uuid::Uuid::from_u128(1),
|
||||
};
|
||||
let morph_id = AssetId::Uuid {
|
||||
uuid: bevy_asset::uuid::Uuid::from_u128(2),
|
||||
};
|
||||
let mut render_meshes = RenderAssets::default();
|
||||
render_meshes.insert(static_id, mesh(None));
|
||||
render_meshes.insert(morph_id, mesh(Some(image.texture_view.clone())));
|
||||
let mut morph_cache = crate::render::mesh::MorphMeshCache::default();
|
||||
assert_eq!(morph_cache.refresh(&render_meshes), &[morph_id]);
|
||||
render_meshes.insert(morph_id, mesh(None));
|
||||
assert!(morph_cache.refresh(&render_meshes).is_empty());
|
||||
render_meshes.insert(static_id, mesh(Some(image.texture_view.clone())));
|
||||
assert_eq!(morph_cache.refresh(&render_meshes), &[static_id]);
|
||||
render_meshes.remove(static_id);
|
||||
assert!(morph_cache.refresh(&render_meshes).is_empty());
|
||||
// Exercise both single-texture and binding-array slab storage.
|
||||
for bindless in [false, true] {
|
||||
let mut slab = LightmapSlab::new(&fallback, bindless);
|
||||
|
||||
Vendored
+6
@@ -145,6 +145,8 @@ pub(crate) struct RenderLightmap {
|
||||
/// system.
|
||||
#[derive(Resource)]
|
||||
pub struct RenderLightmaps {
|
||||
/// Changes whenever slab bindings are added, removed, or replaced.
|
||||
pub(crate) binding_revision: u64,
|
||||
/// The mapping from every lightmapped entity to its lightmap info.
|
||||
///
|
||||
/// Entities without lightmaps, or for which the mesh or lightmap isn't
|
||||
@@ -301,6 +303,7 @@ fn extract_lightmaps(
|
||||
return true;
|
||||
};
|
||||
render_lightmaps.slabs[usize::from(slab_index)].insert(slot_index, gpu_image.clone());
|
||||
render_lightmaps.binding_revision = render_lightmaps.binding_revision.wrapping_add(1);
|
||||
false
|
||||
});
|
||||
}
|
||||
@@ -357,6 +360,7 @@ pub fn init_render_lightmaps(
|
||||
let bindless_supported = binding_arrays_are_usable(&render_device, &render_adapter);
|
||||
|
||||
commands.insert_resource(RenderLightmaps {
|
||||
binding_revision: 0,
|
||||
render_lightmaps: default(),
|
||||
slabs: vec![],
|
||||
free_slabs: FixedBitSet::new(),
|
||||
@@ -369,6 +373,7 @@ impl RenderLightmaps {
|
||||
/// Creates a new slab, appends it to the end of the list, and returns its
|
||||
/// slab index.
|
||||
fn create_slab(&mut self, fallback_images: &FallbackImage) -> LightmapSlabIndex {
|
||||
self.binding_revision = self.binding_revision.wrapping_add(1);
|
||||
let slab_index = LightmapSlabIndex::from(self.slabs.len());
|
||||
self.free_slabs.grow_and_insert(slab_index.into());
|
||||
self.slabs
|
||||
@@ -401,6 +406,7 @@ impl RenderLightmaps {
|
||||
slab_index: LightmapSlabIndex,
|
||||
slot_index: LightmapSlotIndex,
|
||||
) {
|
||||
self.binding_revision = self.binding_revision.wrapping_add(1);
|
||||
let slab = &mut self.slabs[usize::from(slab_index)];
|
||||
slab.remove(fallback_images, slot_index);
|
||||
|
||||
|
||||
Vendored
+150
-31
@@ -2,16 +2,16 @@ use crate::{
|
||||
material_bind_groups::{MaterialBindGroupIndex, MaterialBindGroupSlot},
|
||||
resources::write_atmosphere_buffer,
|
||||
};
|
||||
use bevy_asset::{embedded_asset, load_embedded_asset, AssetId};
|
||||
use bevy_asset::{AssetId, embedded_asset, load_embedded_asset};
|
||||
use bevy_camera::{
|
||||
Camera, Camera3d, Projection,
|
||||
primitives::Aabb,
|
||||
visibility::{NoFrustumCulling, RenderLayers, ViewVisibility, VisibilityRange},
|
||||
Camera, Camera3d, Projection,
|
||||
};
|
||||
use bevy_core_pipeline::{
|
||||
core_3d::{AlphaMask3d, Opaque3d, Transmissive3d, Transparent3d, CORE_3D_DEPTH_FORMAT},
|
||||
core_3d::{AlphaMask3d, CORE_3D_DEPTH_FORMAT, Opaque3d, Transmissive3d, Transparent3d},
|
||||
deferred::{AlphaMask3dDeferred, Opaque3dDeferred},
|
||||
oit::{prepare_oit_buffers, OrderIndependentTransparencySettingsOffset},
|
||||
oit::{OrderIndependentTransparencySettingsOffset, prepare_oit_buffers},
|
||||
prepass::MotionVectorPrepass,
|
||||
};
|
||||
use bevy_derive::{Deref, DerefMut};
|
||||
@@ -21,7 +21,7 @@ use bevy_ecs::{
|
||||
prelude::*,
|
||||
query::{QueryData, ROQueryItem},
|
||||
relationship::RelationshipSourceCollection,
|
||||
system::{lifetimeless::*, SystemParamItem, SystemState},
|
||||
system::{SystemParamItem, SystemState, lifetimeless::*},
|
||||
};
|
||||
use bevy_image::{BevyDefault, ImageSampler, TextureFormatPixelInfo};
|
||||
use bevy_light::{
|
||||
@@ -30,20 +30,22 @@ use bevy_light::{
|
||||
};
|
||||
use bevy_math::{Affine3, Rect, UVec2, Vec3, Vec4};
|
||||
use bevy_mesh::{
|
||||
skinning::SkinnedMesh, BaseMeshPipelineKey, Mesh, Mesh3d, MeshTag, MeshVertexBufferLayoutRef,
|
||||
VertexAttributeDescriptor,
|
||||
BaseMeshPipelineKey, Mesh, Mesh3d, MeshTag, MeshVertexBufferLayoutRef,
|
||||
VertexAttributeDescriptor, skinning::SkinnedMesh,
|
||||
};
|
||||
use bevy_platform::collections::{hash_map::Entry, HashMap};
|
||||
use bevy_platform::collections::{HashMap, hash_map::Entry};
|
||||
use bevy_render::{
|
||||
Extract,
|
||||
batching::{
|
||||
GetBatchData, GetFullBatchData, NoAutomaticBatching,
|
||||
gpu_preprocessing::{
|
||||
self, GpuPreprocessingSupport, IndirectBatchSet, IndirectParametersBuffers,
|
||||
IndirectParametersCpuMetadata, IndirectParametersIndexed, IndirectParametersNonIndexed,
|
||||
InstanceInputUniformBuffer, UntypedPhaseIndirectParametersBuffers,
|
||||
},
|
||||
no_gpu_preprocessing, GetBatchData, GetFullBatchData, NoAutomaticBatching,
|
||||
no_gpu_preprocessing,
|
||||
},
|
||||
mesh::{allocator::MeshAllocator, RenderMesh, RenderMeshBufferInfo},
|
||||
mesh::{RenderMesh, RenderMeshBufferInfo, allocator::MeshAllocator},
|
||||
render_asset::RenderAssets,
|
||||
render_phase::{
|
||||
BinnedRenderPhasePlugin, InputUniformIndex, PhaseItem, PhaseItemExtraIndex, RenderCommand,
|
||||
@@ -57,11 +59,10 @@ use bevy_render::{
|
||||
self, NoIndirectDrawing, RenderVisibilityRanges, RetainedViewEntity, ViewTarget,
|
||||
ViewUniformOffset,
|
||||
},
|
||||
Extract,
|
||||
};
|
||||
use bevy_shader::{load_shader_library, Shader, ShaderDefVal, ShaderSettings};
|
||||
use bevy_shader::{Shader, ShaderDefVal, ShaderSettings, load_shader_library};
|
||||
use bevy_transform::components::GlobalTransform;
|
||||
use bevy_utils::{default, Parallel, TypeIdMap};
|
||||
use bevy_utils::{Parallel, TypeIdMap, default};
|
||||
use core::any::TypeId;
|
||||
use core::mem::size_of;
|
||||
use material_bind_groups::MaterialBindingId;
|
||||
@@ -71,8 +72,8 @@ use self::irradiance_volume::IRRADIANCE_VOLUMES_ARE_USABLE;
|
||||
use crate::{
|
||||
render::{
|
||||
morph::{
|
||||
extract_morphs, no_automatic_morph_batching, prepare_morphs, MorphIndices,
|
||||
MorphUniforms,
|
||||
MorphIndices, MorphUniforms, extract_morphs, no_automatic_morph_batching,
|
||||
prepare_morphs,
|
||||
},
|
||||
skin::no_automatic_skin_batching,
|
||||
},
|
||||
@@ -83,15 +84,15 @@ use bevy_core_pipeline::prepass::{DeferredPrepass, DepthPrepass, NormalPrepass};
|
||||
use bevy_core_pipeline::tonemapping::{DebandDither, Tonemapping};
|
||||
use bevy_ecs::change_detection::Tick;
|
||||
use bevy_ecs::system::SystemChangeTick;
|
||||
use bevy_render::RenderSystems::PrepareAssets;
|
||||
use bevy_render::camera::TemporalJitter;
|
||||
use bevy_render::prelude::Msaa;
|
||||
use bevy_render::sync_world::{MainEntity, MainEntityHashMap};
|
||||
use bevy_render::view::ExtractedView;
|
||||
use bevy_render::RenderSystems::PrepareAssets;
|
||||
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use nonmax::{NonMaxU16, NonMaxU32};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use static_assertions::const_assert_eq;
|
||||
|
||||
/// Provides support for rendering 3D meshes.
|
||||
@@ -2749,7 +2750,97 @@ pub struct MeshPhaseBindGroups {
|
||||
model_only: Option<BindGroup>,
|
||||
skinned: Option<MeshBindGroupPair>,
|
||||
morph_targets: HashMap<AssetId<Mesh>, MeshBindGroupPair>,
|
||||
lightmaps: HashMap<LightmapSlabIndex, BindGroup>,
|
||||
lightmaps: std::sync::Arc<HashMap<LightmapSlabIndex, BindGroup>>,
|
||||
}
|
||||
|
||||
/// Cached lightmap lookup table for one rendering phase.
|
||||
#[derive(Default)]
|
||||
pub struct PhaseLightmapCache {
|
||||
key: Option<LightmapTableKey>,
|
||||
groups: std::sync::Arc<HashMap<LightmapSlabIndex, BindGroup>>,
|
||||
previous: Option<Box<PhaseLightmapCache>>,
|
||||
}
|
||||
|
||||
/// Meshes that actually need morph bindings, refreshed when GPU assets change.
|
||||
#[derive(Default)]
|
||||
pub struct MorphMeshCache(Vec<AssetId<Mesh>>);
|
||||
impl MorphMeshCache {
|
||||
pub(crate) fn refresh(&mut self, meshes: &RenderAssets<RenderMesh>) -> &[AssetId<Mesh>] {
|
||||
self.0 = meshes
|
||||
.iter()
|
||||
.filter_map(|(id, mesh)| mesh.morph_targets.as_ref().map(|_| id))
|
||||
.collect();
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
struct LightmapTableKey {
|
||||
buffer: Buffer,
|
||||
offset: u64,
|
||||
size: Option<BufferSize>,
|
||||
layout: BindGroupLayoutId,
|
||||
revision: u64,
|
||||
}
|
||||
impl PhaseLightmapCache {
|
||||
pub(crate) fn select(
|
||||
&mut self,
|
||||
model: &BindingResource,
|
||||
layout: BindGroupLayoutId,
|
||||
revision: u64,
|
||||
) -> bool {
|
||||
if self.matches(model, layout, revision) {
|
||||
return true;
|
||||
}
|
||||
if self
|
||||
.previous
|
||||
.as_ref()
|
||||
.is_some_and(|previous| previous.matches(model, layout, revision))
|
||||
{
|
||||
let mut previous = self.previous.take().unwrap();
|
||||
core::mem::swap(self, &mut previous);
|
||||
self.previous = Some(previous);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
pub(crate) fn matches(
|
||||
&self,
|
||||
model: &BindingResource,
|
||||
layout: BindGroupLayoutId,
|
||||
revision: u64,
|
||||
) -> bool {
|
||||
let (Some(key), BindingResource::Buffer(model)) = (&self.key, model) else {
|
||||
return false;
|
||||
};
|
||||
&*key.buffer == model.buffer
|
||||
&& key.offset == model.offset
|
||||
&& key.size == model.size
|
||||
&& key.layout == layout
|
||||
&& key.revision == revision
|
||||
}
|
||||
pub(crate) fn remember(
|
||||
&mut self,
|
||||
model: &BindingResource,
|
||||
layout: BindGroupLayoutId,
|
||||
revision: u64,
|
||||
) {
|
||||
if self.key.is_some() {
|
||||
self.previous = Some(Box::new(PhaseLightmapCache {
|
||||
key: self.key.take(),
|
||||
groups: core::mem::take(&mut self.groups),
|
||||
previous: None,
|
||||
}));
|
||||
}
|
||||
self.key = match model {
|
||||
BindingResource::Buffer(model) => Some(LightmapTableKey {
|
||||
buffer: Buffer::from(model.buffer.clone()),
|
||||
offset: model.offset,
|
||||
size: model.size,
|
||||
layout,
|
||||
revision,
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MeshBindGroupPair {
|
||||
@@ -2773,7 +2864,7 @@ impl MeshPhaseBindGroups {
|
||||
self.model_only = None;
|
||||
self.skinned = None;
|
||||
self.morph_targets.clear();
|
||||
self.lightmaps.clear();
|
||||
self.lightmaps = default();
|
||||
}
|
||||
/// Get the `BindGroup` for `RenderMesh` with given `handle_id` and lightmap
|
||||
/// key `lightmap`.
|
||||
@@ -2826,7 +2917,12 @@ pub fn prepare_mesh_bind_groups(
|
||||
skins_uniform: Res<SkinUniforms>,
|
||||
weights_uniform: Res<MorphUniforms>,
|
||||
mut render_lightmaps: ResMut<RenderLightmaps>,
|
||||
mut lightmap_tables: Local<TypeIdMap<PhaseLightmapCache>>,
|
||||
mut morph_meshes: Local<MorphMeshCache>,
|
||||
) {
|
||||
if meshes.is_changed() {
|
||||
morph_meshes.refresh(&meshes);
|
||||
}
|
||||
// CPU mesh preprocessing path.
|
||||
if let Some(cpu_batched_instance_buffer) = cpu_batched_instance_buffer
|
||||
&& let Some(instance_data_binding) = cpu_batched_instance_buffer
|
||||
@@ -2843,6 +2939,8 @@ pub fn prepare_mesh_bind_groups(
|
||||
&skins_uniform,
|
||||
&weights_uniform,
|
||||
&mut render_lightmaps,
|
||||
lightmap_tables.entry(TypeId::of::<()>()).or_default(),
|
||||
&morph_meshes.0,
|
||||
);
|
||||
|
||||
commands.insert_resource(MeshBindGroups::CpuPreprocessing(
|
||||
@@ -2874,11 +2972,18 @@ pub fn prepare_mesh_bind_groups(
|
||||
&skins_uniform,
|
||||
&weights_uniform,
|
||||
&mut render_lightmaps,
|
||||
lightmap_tables.entry(*phase_type_id).or_default(),
|
||||
&morph_meshes.0,
|
||||
);
|
||||
|
||||
gpu_preprocessing_mesh_bind_groups.insert(*phase_type_id, mesh_phase_bind_groups);
|
||||
}
|
||||
|
||||
lightmap_tables.retain(|phase, _| {
|
||||
gpu_batched_instance_buffers
|
||||
.phase_instance_buffers
|
||||
.contains_key(phase)
|
||||
});
|
||||
commands.insert_resource(MeshBindGroups::GpuPreprocessing(
|
||||
gpu_preprocessing_mesh_bind_groups,
|
||||
));
|
||||
@@ -2895,6 +3000,8 @@ fn prepare_mesh_bind_groups_for_phase(
|
||||
skins_uniform: &SkinUniforms,
|
||||
weights_uniform: &MorphUniforms,
|
||||
render_lightmaps: &mut RenderLightmaps,
|
||||
lightmap_table: &mut PhaseLightmapCache,
|
||||
morph_meshes: &[AssetId<Mesh>],
|
||||
) -> MeshPhaseBindGroups {
|
||||
let layouts = &mesh_pipeline.mesh_layouts;
|
||||
|
||||
@@ -2922,7 +3029,10 @@ fn prepare_mesh_bind_groups_for_phase(
|
||||
// group.
|
||||
if let Some(weights) = weights_uniform.current_buffer.buffer() {
|
||||
let prev_weights = weights_uniform.prev_buffer.buffer().unwrap_or(weights);
|
||||
for (id, gpu_mesh) in meshes.iter() {
|
||||
for (id, gpu_mesh) in morph_meshes
|
||||
.iter()
|
||||
.filter_map(|&id| meshes.get(id).map(|mesh| (id, mesh)))
|
||||
{
|
||||
if let Some(targets) = gpu_mesh.morph_targets.as_ref() {
|
||||
let bind_group_pair = if is_skinned(&gpu_mesh.layout) {
|
||||
let prev_skin = &skins_uniform.prev_buffer;
|
||||
@@ -2972,18 +3082,27 @@ fn prepare_mesh_bind_groups_for_phase(
|
||||
|
||||
// Create lightmap bindgroups. There will be one bindgroup for each slab.
|
||||
let bindless_supported = render_lightmaps.bindless_supported;
|
||||
for (lightmap_slab_id, lightmap_slab) in render_lightmaps.slabs.iter_mut().enumerate() {
|
||||
groups.lightmaps.insert(
|
||||
LightmapSlabIndex(NonMaxU32::new(lightmap_slab_id as u32).unwrap()),
|
||||
layouts.lightmapped(
|
||||
render_device,
|
||||
pipeline_cache,
|
||||
&model,
|
||||
lightmap_slab,
|
||||
bindless_supported,
|
||||
),
|
||||
);
|
||||
let layout = pipeline_cache
|
||||
.get_bind_group_layout(&layouts.lightmapped)
|
||||
.id();
|
||||
if !lightmap_table.select(&model, layout, render_lightmaps.binding_revision) {
|
||||
let mut bindings = HashMap::default();
|
||||
for (lightmap_slab_id, lightmap_slab) in render_lightmaps.slabs.iter_mut().enumerate() {
|
||||
bindings.insert(
|
||||
LightmapSlabIndex(NonMaxU32::new(lightmap_slab_id as u32).unwrap()),
|
||||
layouts.lightmapped(
|
||||
render_device,
|
||||
pipeline_cache,
|
||||
&model,
|
||||
lightmap_slab,
|
||||
bindless_supported,
|
||||
),
|
||||
);
|
||||
}
|
||||
lightmap_table.remember(&model, layout, render_lightmaps.binding_revision);
|
||||
lightmap_table.groups = std::sync::Arc::new(bindings);
|
||||
}
|
||||
groups.lightmaps = lightmap_table.groups.clone();
|
||||
|
||||
groups
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user