mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge pull request #7861 from horizon-rust/perf/sprite-cpu-copy
🤖 Agent: block sprite pixels lz4-packed once uploaded (-19 MB live heap); one risk to know: SpritePixels::get returns zeroed pixels if decompression ever failed, which would show as black textures on a rebuilt renderer (device loss, split screen), not a crash. rdbtCVS's scene_replay adaptation included; verify green on this head, merger waits for parity.
This commit is contained in:
4 files changed
+108
-7
No files matched your search
@@ -190,8 +190,9 @@ fn replay(window: Arc<Window>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
let one = [255u8; 4];
|
||||
let zero = [0u8; 4];
|
||||
let pixels = render.pixels.get();
|
||||
let prepared = PreparedTextures::new(
|
||||
&render.pixels,
|
||||
&pixels,
|
||||
render.sprite_size,
|
||||
render.texture_layers,
|
||||
&render.sprite_mips,
|
||||
@@ -206,7 +207,7 @@ fn replay(window: Arc<Window>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut renderer = pollster::block_on(Renderer::new(
|
||||
window,
|
||||
true,
|
||||
&render.pixels,
|
||||
&pixels,
|
||||
render.sprite_size,
|
||||
render.sprite_minimum_size,
|
||||
render.texture_layers,
|
||||
@@ -224,6 +225,8 @@ fn replay(window: Arc<Window>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
false,
|
||||
false,
|
||||
))?;
|
||||
drop(pixels);
|
||||
render.pixels.pack();
|
||||
let state = &scene.renderer;
|
||||
let [width, height] = o.size.unwrap_or(state.size);
|
||||
renderer.resize(width, height);
|
||||
|
||||
@@ -84,7 +84,9 @@ pub struct Assets {
|
||||
pub struct RenderAssets {
|
||||
pub sprite_size: u32,
|
||||
pub sprite_minimum_size: u32,
|
||||
pub pixels: Vec<u8>,
|
||||
/// The block sprites' RGBA, layer by layer: as built until a renderer has
|
||||
/// them, then kept compressed ([`SpritePixels`]).
|
||||
pub pixels: SpritePixels,
|
||||
pub texture_layers: u32,
|
||||
pub model_catalog: crate::models::StateModels,
|
||||
/// Shared parsed resources; item-only validation errors surface at item preparation.
|
||||
@@ -111,6 +113,76 @@ pub struct RenderAssets {
|
||||
/// sprites are when it changes (`native_block_sprites_version`).
|
||||
pub generation: u64,
|
||||
}
|
||||
/// The block sprites' pixels for a renderer to upload. Once a renderer has
|
||||
/// them ([`Self::pack`]) they are kept lz4-compressed: the GPU holds the
|
||||
/// texture array, and the copy only serves a renderer made again (another
|
||||
/// window, a lost device, another graphics API), which waits for
|
||||
/// [`Self::get`] to uncompress them. Shared by the window and the workers'
|
||||
/// `RenderAssets`, so the compressed form replaces the raw one in place.
|
||||
pub struct SpritePixels(Arc<PixelStore>);
|
||||
struct PixelStore {
|
||||
len: usize,
|
||||
/// Set by the first `pack`: later calls cost one atomic read.
|
||||
packing: std::sync::atomic::AtomicBool,
|
||||
/// As built; dropped once `packed` holds them.
|
||||
raw: std::sync::Mutex<Option<Arc<Vec<u8>>>>,
|
||||
packed: std::sync::OnceLock<Vec<u8>>,
|
||||
}
|
||||
impl SpritePixels {
|
||||
pub fn new(pixels: Vec<u8>) -> Self {
|
||||
Self(Arc::new(PixelStore {
|
||||
len: pixels.len(),
|
||||
packing: std::sync::atomic::AtomicBool::new(false),
|
||||
raw: std::sync::Mutex::new(Some(Arc::new(pixels))),
|
||||
packed: std::sync::OnceLock::new(),
|
||||
}))
|
||||
}
|
||||
/// Their size in bytes, uncompressed.
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len
|
||||
}
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.len == 0
|
||||
}
|
||||
/// The pixels as built, uncompressed again once packed.
|
||||
pub fn get(&self) -> Arc<Vec<u8>> {
|
||||
let raw = self.0.raw.lock().unwrap_or_else(|e| e.into_inner()).clone();
|
||||
raw.unwrap_or_else(|| {
|
||||
let packed = self.0.packed.get().map(Vec::as_slice).unwrap_or_default();
|
||||
Arc::new(
|
||||
lz4_flex::decompress_size_prepended(packed)
|
||||
.ok()
|
||||
.filter(|p| p.len() == self.0.len)
|
||||
.unwrap_or_else(|| vec![0; self.0.len]),
|
||||
)
|
||||
})
|
||||
}
|
||||
/// Keep them compressed from now on, once a renderer has uploaded them:
|
||||
/// compressed on a thread of their own, the raw copy dropped after.
|
||||
pub fn pack(&self) {
|
||||
use std::sync::atomic::Ordering;
|
||||
if self.0.len == 0 || self.0.packing.swap(true, Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let store = self.0.clone();
|
||||
let job = move || {
|
||||
let raw = store.raw.lock().unwrap_or_else(|e| e.into_inner()).clone();
|
||||
if let Some(raw) = raw {
|
||||
let _ = store.packed.set(lz4_flex::compress_prepend_size(&raw));
|
||||
// `get` reads the compressed copy from here on.
|
||||
*store.raw.lock().unwrap_or_else(|e| e.into_inner()) = None;
|
||||
}
|
||||
};
|
||||
let fallback = job.clone();
|
||||
if std::thread::Builder::new()
|
||||
.name("Sprite packing".into())
|
||||
.spawn(job)
|
||||
.is_err()
|
||||
{
|
||||
fallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
/// The last `RenderAssets::generation` handed out.
|
||||
static GENERATION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
fn invalid(message: &str) -> io::Error {
|
||||
@@ -333,7 +405,7 @@ impl RenderAssets {
|
||||
sprite_size: sprites.size,
|
||||
sprite_minimum_size: sprites.minimum_size,
|
||||
texture_layers: u32::try_from(sprites.count())?,
|
||||
pixels: sprites.pixels,
|
||||
pixels: SpritePixels::new(sprites.pixels),
|
||||
model_catalog: models.catalog,
|
||||
model_library,
|
||||
model_placements: models.placements,
|
||||
@@ -431,4 +503,29 @@ mod tests {
|
||||
[ConsumeEffect::ApplyEffects { probability, .. }] if probability == 0.3
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sprite_pixels_come_back_the_same_once_packed() {
|
||||
// Two layers of 16x16 RGBA with some structure, as sprites have.
|
||||
let pixels: Vec<u8> = (0..2 * 16 * 16 * 4).map(|i| (i * 7 % 251) as u8).collect();
|
||||
let stored = super::SpritePixels::new(pixels.clone());
|
||||
assert_eq!(*stored.get(), pixels);
|
||||
stored.pack();
|
||||
stored.pack();
|
||||
// The packing thread drops the raw copy once the compressed one is set.
|
||||
let started = std::time::Instant::now();
|
||||
while stored.0.raw.lock().unwrap().is_some() {
|
||||
assert!(started.elapsed().as_secs() < 10, "packing never finished");
|
||||
std::thread::yield_now();
|
||||
}
|
||||
assert!(
|
||||
stored
|
||||
.0
|
||||
.packed
|
||||
.get()
|
||||
.is_some_and(|p| p.len() < pixels.len())
|
||||
);
|
||||
assert_eq!(*stored.get(), pixels);
|
||||
assert_eq!(stored.len(), pixels.len());
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ use std::{
|
||||
|
||||
fn digest(render: &RenderAssets) -> u64 {
|
||||
let mut h = DefaultHasher::new();
|
||||
render.pixels.hash(&mut h);
|
||||
render.pixels.get().hash(&mut h);
|
||||
(
|
||||
render.sprite_size,
|
||||
render.sprite_minimum_size,
|
||||
|
||||
@@ -15625,6 +15625,7 @@ impl Game<'_> {
|
||||
// compressed (a no-op once it is).
|
||||
if self.renderer.is_some() {
|
||||
self.gui.atlas.pack_pixels();
|
||||
self.pack.render.pixels.pack();
|
||||
}
|
||||
// Before any text of this frame: a full glyph region starts over.
|
||||
self.gui.atlas.font.begin_frame();
|
||||
@@ -20682,7 +20683,7 @@ fn load_world_rest(
|
||||
fn prepare_renderer_textures(pack: &PackResources) -> horizon_client::gfx::PreparedTextures {
|
||||
let assets = &pack.render;
|
||||
horizon_client::gfx::PreparedTextures::new(
|
||||
&assets.pixels,
|
||||
&assets.pixels.get(),
|
||||
assets.sprite_size,
|
||||
assets.texture_layers,
|
||||
&assets.sprite_mips,
|
||||
@@ -20728,7 +20729,7 @@ fn build_renderer(
|
||||
let mut renderer = pollster::block_on(Renderer::new(
|
||||
window,
|
||||
offscreen,
|
||||
&assets.pixels,
|
||||
&assets.pixels.get(),
|
||||
assets.sprite_size,
|
||||
assets.sprite_minimum_size,
|
||||
assets.texture_layers,
|
||||
|
||||
Reference in new issue
Block a user