mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge pull request #10328 from rison1337/perf/cache-name-tag-layout
Cache unchanged entity name-tag glyph layouts
This commit is contained in:
4 files changed
+348
-9
No files matched your search
@@ -2955,6 +2955,7 @@ struct Game<'a> {
|
||||
dragon_model: Option<horizon_client::mobs::Part>,
|
||||
/// Map textures waiting for the next HUD batch to upload them.
|
||||
map_patches: Vec<horizon_client::font::Patch>,
|
||||
name_tag_layouts: names::NameTagLayouts,
|
||||
text_displays: std::collections::HashMap<i32, text_displays::RemoteTextDisplay>,
|
||||
interactions: std::collections::HashMap<i32, interactions::RemoteInteraction>,
|
||||
/// The fence knots a server tracks to this client, by entity id, at their
|
||||
@@ -20054,6 +20055,7 @@ impl<'a> Game<'a> {
|
||||
breath_clouds: Vec::new(),
|
||||
dragon_model: prepared.dragon_model,
|
||||
map_patches: Vec::new(),
|
||||
name_tag_layouts: Default::default(),
|
||||
text_displays: Default::default(),
|
||||
interactions: Default::default(),
|
||||
remote_knots: Default::default(),
|
||||
|
||||
@@ -8,13 +8,112 @@
|
||||
//! and the player's own in third person with the `ownNameTag` extra.
|
||||
use super::{Game, view_vector};
|
||||
use horizon_client::{
|
||||
block_entity_render::vertex_light, hud::HudBatch, mesher::Vertex, mobs::Mob, mod_api,
|
||||
save::uuid_text, scoreboard as sb, text::Text,
|
||||
block_entity_render::vertex_light,
|
||||
hud::{HudAtlas, HudBatch, HudVertex},
|
||||
mesher::Vertex,
|
||||
mobs::Mob,
|
||||
mod_api,
|
||||
save::uuid_text,
|
||||
scoreboard as sb,
|
||||
text::Text,
|
||||
};
|
||||
|
||||
/// The world size of one text pixel in a name tag: `Matrix4f.scale(0.025F, -0.025F, 0.025F)`.
|
||||
pub(super) const NAME_TAG_UNIT: f32 = 0.025;
|
||||
|
||||
/// Local glyph quads only: camera transforms, lighting and cosmetic marks stay per frame.
|
||||
/// Slots follow submission order; comparing the whole styled text also handles entity reorder,
|
||||
/// team/score changes and native name hooks without keeping entity IDs alive.
|
||||
#[derive(Default)]
|
||||
pub(super) struct NameTagLayouts {
|
||||
epoch: (u64, u64, bool),
|
||||
next: usize,
|
||||
lines: Vec<Option<NameTagLine>>,
|
||||
}
|
||||
|
||||
struct NameTagLine {
|
||||
text: Text,
|
||||
alpha: u32,
|
||||
background: bool,
|
||||
y: f32,
|
||||
behind: usize,
|
||||
vertices: Vec<HudVertex>,
|
||||
}
|
||||
|
||||
impl NameTagLayouts {
|
||||
fn begin_frame(&mut self, atlas: &HudAtlas) {
|
||||
let epoch = (
|
||||
atlas.revision,
|
||||
atlas.font.epoch(),
|
||||
horizon_client::lang::bidirectional(),
|
||||
);
|
||||
if self.epoch != epoch {
|
||||
self.lines.clear();
|
||||
self.epoch = epoch;
|
||||
}
|
||||
self.next = 0;
|
||||
}
|
||||
|
||||
fn end_frame(&mut self) {
|
||||
self.lines.truncate(self.next);
|
||||
}
|
||||
|
||||
fn name_tag(
|
||||
&mut self,
|
||||
batch: &mut HudBatch,
|
||||
atlas: &HudAtlas,
|
||||
text: &Text,
|
||||
y: f32,
|
||||
alpha: u32,
|
||||
background: bool,
|
||||
) -> usize {
|
||||
// At most 512 * 512 HUD vertices (~10 MiB), plus short text keys. Longer tags and
|
||||
// excess lines still render normally. Animated/random glyphs and live inline skins
|
||||
// must go through the font each time, including legacy formatting supplied by hooks.
|
||||
const MAX_LINES: usize = 512;
|
||||
const MAX_VERTICES: usize = 512;
|
||||
let slot = self.next;
|
||||
self.next += 1;
|
||||
let stable =
|
||||
text.resolved().spans.iter().all(|span| {
|
||||
span.style.obfuscated != Some(true) && span.style.inline_object.is_none()
|
||||
}) && text.spans.iter().map(|s| s.text.len()).sum::<usize>() <= 1024;
|
||||
if slot < MAX_LINES
|
||||
&& stable
|
||||
&& let Some(Some(line)) = self.lines.get(slot)
|
||||
&& line.text == *text
|
||||
&& line.alpha == alpha
|
||||
&& line.background == background
|
||||
&& line.y == y
|
||||
{
|
||||
batch.vertices.extend_from_slice(&line.vertices);
|
||||
return line.behind;
|
||||
}
|
||||
let behind = batch.name_tag(
|
||||
atlas,
|
||||
text,
|
||||
y,
|
||||
alpha << 24 | 0xFF_FFFF,
|
||||
if background { 0x3F00_0000 } else { 0 },
|
||||
);
|
||||
if slot < MAX_LINES {
|
||||
if self.lines.len() <= slot {
|
||||
self.lines.resize_with(slot + 1, || None);
|
||||
}
|
||||
self.lines[slot] =
|
||||
(stable && batch.vertices.len() <= MAX_VERTICES).then(|| NameTagLine {
|
||||
text: text.clone(),
|
||||
alpha,
|
||||
background,
|
||||
y,
|
||||
behind,
|
||||
vertices: batch.vertices.clone(),
|
||||
});
|
||||
}
|
||||
behind
|
||||
}
|
||||
}
|
||||
|
||||
impl Game<'_> {
|
||||
/// A named name tag names the mob under the crosshair.
|
||||
pub(super) fn name_mob(&mut self, index: usize) -> bool {
|
||||
@@ -469,6 +568,8 @@ impl Game<'_> {
|
||||
// max((0.25 + 0.75) / 2, 0.5), as ARGB.color (the floor of 255 times it).
|
||||
const FADED: u32 = 0x7F;
|
||||
let mut patches = Vec::new();
|
||||
let mut layouts = std::mem::take(&mut self.name_tag_layouts);
|
||||
layouts.begin_frame(&self.gui.atlas);
|
||||
for tag in self.name_tags(camera, alpha) {
|
||||
let o = tag.at - camera;
|
||||
// `gbuffers_entities` draws name tag text (2.3); a sneaking player's faded tag, on its
|
||||
@@ -486,6 +587,7 @@ impl Game<'_> {
|
||||
NAME_TAG_UNIT,
|
||||
out,
|
||||
tag.marks,
|
||||
Some(&mut layouts),
|
||||
));
|
||||
continue;
|
||||
}
|
||||
@@ -503,6 +605,7 @@ impl Game<'_> {
|
||||
NAME_TAG_UNIT,
|
||||
out,
|
||||
tag.marks,
|
||||
Some(&mut layouts),
|
||||
));
|
||||
// RenderPipelines.TEXT_SEE_THROUGH: no depth test, no lightmap.
|
||||
patches.extend(self.styled_name_tag_quads(
|
||||
@@ -515,8 +618,11 @@ impl Game<'_> {
|
||||
NAME_TAG_UNIT,
|
||||
see_through,
|
||||
tag.marks,
|
||||
Some(&mut layouts),
|
||||
));
|
||||
}
|
||||
layouts.end_frame();
|
||||
self.name_tag_layouts = layouts;
|
||||
self.map_patches.extend(patches);
|
||||
}
|
||||
/// The lines of one name tag, `o` from the camera: the text at
|
||||
@@ -545,6 +651,7 @@ impl Game<'_> {
|
||||
unit,
|
||||
out,
|
||||
Marks::default(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -563,6 +670,7 @@ impl Game<'_> {
|
||||
unit: f32,
|
||||
out: &mut Vec<Vertex>,
|
||||
marks: Marks,
|
||||
mut layouts: Option<&mut NameTagLayouts>,
|
||||
) -> Vec<horizon_client::font::Patch> {
|
||||
// Options.getBackgroundOpacity(0.25) black, as ARGB.color.
|
||||
const BACKGROUND: u32 = 0x3F00_0000;
|
||||
@@ -595,13 +703,23 @@ impl Game<'_> {
|
||||
batch.vertices.clear();
|
||||
// Font.prepareText, as any text: bold, italic, fonts and each
|
||||
// glyph's own box (accented.png's are 12 pixels tall).
|
||||
let behind = batch.name_tag(
|
||||
&self.gui.atlas,
|
||||
text,
|
||||
y0,
|
||||
text_alpha << 24 | 0xFF_FFFF,
|
||||
if background { BACKGROUND } else { 0 },
|
||||
);
|
||||
let behind = match layouts.as_deref_mut() {
|
||||
Some(layouts) => layouts.name_tag(
|
||||
&mut batch,
|
||||
&self.gui.atlas,
|
||||
text,
|
||||
y0,
|
||||
text_alpha,
|
||||
background,
|
||||
),
|
||||
None => batch.name_tag(
|
||||
&self.gui.atlas,
|
||||
text,
|
||||
y0,
|
||||
text_alpha << 24 | 0xFF_FFFF,
|
||||
if background { BACKGROUND } else { 0 },
|
||||
),
|
||||
};
|
||||
if n == last && marks.horizon {
|
||||
let x = -self.gui.atlas.font.width_text(text) / 2.0 - 10.0;
|
||||
// 8 text pixels square, a pixel left of the background.
|
||||
@@ -777,6 +895,135 @@ mod tests {
|
||||
use horizon_core::geometry::Vec3;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn tag_font() -> HudAtlas {
|
||||
static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"horizon-tag-cache-{}-{}",
|
||||
std::process::id(),
|
||||
NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
|
||||
));
|
||||
let textures = root.join("assets/minecraft/textures/font");
|
||||
std::fs::create_dir_all(root.join("assets/minecraft/font")).unwrap();
|
||||
std::fs::create_dir_all(&textures).unwrap();
|
||||
std::fs::write(root.join("assets/minecraft/font/default.json"),
|
||||
r#"{"providers":[{"type":"bitmap","file":"minecraft:font/cache.png","ascent":7,"chars":["ab"]}]}"#).unwrap();
|
||||
let file = std::fs::File::create(textures.join("cache.png")).unwrap();
|
||||
let mut encoder = png::Encoder::new(file, 16, 8);
|
||||
encoder.set_color(png::ColorType::Rgba);
|
||||
encoder.set_depth(png::BitDepth::Eight);
|
||||
let mut writer = encoder.write_header().unwrap();
|
||||
let pixels: Vec<u8> = (0..128)
|
||||
.flat_map(|i| [255, 255, 255, if i % 8 < 4 { 255 } else { 0 }])
|
||||
.collect();
|
||||
writer.write_image_data(&pixels).unwrap();
|
||||
drop(writer);
|
||||
let mut atlas = HudAtlas::boot();
|
||||
atlas.font = horizon_client::font::Font::load(
|
||||
&root,
|
||||
&Default::default(),
|
||||
&[],
|
||||
(1024, 1024),
|
||||
(512, 512),
|
||||
atlas.sprites["horizon/white"],
|
||||
);
|
||||
std::fs::remove_dir_all(root).unwrap();
|
||||
atlas
|
||||
}
|
||||
|
||||
fn tag_bytes(
|
||||
cache: &mut NameTagLayouts,
|
||||
atlas: &HudAtlas,
|
||||
text: &Text,
|
||||
alpha: u32,
|
||||
background: bool,
|
||||
) -> Vec<u8> {
|
||||
cache.begin_frame(atlas);
|
||||
let mut batch = HudBatch::new([1, 1]);
|
||||
let behind = cache.name_tag(&mut batch, atlas, text, 0.0, alpha, background);
|
||||
cache.end_frame();
|
||||
let mut fresh = HudBatch::new([1, 1]);
|
||||
assert_eq!(
|
||||
behind,
|
||||
fresh.name_tag(
|
||||
atlas,
|
||||
text,
|
||||
0.0,
|
||||
alpha << 24 | 0xFF_FFFF,
|
||||
if background { 0x3F00_0000 } else { 0 }
|
||||
)
|
||||
);
|
||||
let bytes: &[u8] = bytemuck::cast_slice(&batch.vertices);
|
||||
assert_eq!(bytes, bytemuck::cast_slice::<_, u8>(&fresh.vertices));
|
||||
bytes.to_vec()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cached_tag_quads_follow_text_style_pass_and_font_changes() {
|
||||
let mut atlas = tag_font();
|
||||
let mut cache = NameTagLayouts::default();
|
||||
let a = Text::literal("a");
|
||||
let first = tag_bytes(&mut cache, &atlas, &a, 255, false);
|
||||
assert!(!first.is_empty());
|
||||
assert_eq!(tag_bytes(&mut cache, &atlas, &a, 255, false), first);
|
||||
for text in [
|
||||
Text::literal("b"),
|
||||
a.clone().bold(),
|
||||
Text::colored("a", 0x12AB34),
|
||||
] {
|
||||
assert_ne!(tag_bytes(&mut cache, &atlas, &text, 255, false), first);
|
||||
tag_bytes(&mut cache, &atlas, &text, 255, false);
|
||||
}
|
||||
assert_ne!(tag_bytes(&mut cache, &atlas, &a, 127, true), first);
|
||||
tag_bytes(&mut cache, &atlas, &a, 127, true);
|
||||
tag_bytes(&mut cache, &atlas, &a, 255, false);
|
||||
// Reset and occupy the first glyph slot with b: a's old UV is now wrong.
|
||||
atlas.font.reset_cache();
|
||||
atlas.font.glyph('b');
|
||||
assert_ne!(tag_bytes(&mut cache, &atlas, &a, 255, false), first);
|
||||
// Font options may change without replacing the atlas.
|
||||
atlas.font.uniform = true;
|
||||
tag_bytes(&mut cache, &atlas, &a, 255, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn obfuscated_tag_glyphs_keep_animating_after_cache_warmup() {
|
||||
let atlas = tag_font();
|
||||
let mut cache = NameTagLayouts::default();
|
||||
let text = Text::legacy("§kaaaaaaaaaaaaaaaa");
|
||||
let mut images = std::collections::HashSet::new();
|
||||
for seed in 0..8 {
|
||||
cache.begin_frame(&atlas);
|
||||
atlas.font.reseed(seed);
|
||||
let mut batch = HudBatch::new([1, 1]);
|
||||
cache.name_tag(&mut batch, &atlas, &text, 0.0, 255, false);
|
||||
cache.end_frame();
|
||||
let bytes = bytemuck::cast_slice::<_, u8>(&batch.vertices).to_vec();
|
||||
atlas.font.reseed(seed);
|
||||
let mut fresh = HudBatch::new([1, 1]);
|
||||
fresh.name_tag(&atlas, &text, 0.0, u32::MAX, 0);
|
||||
assert_eq!(bytes, bytemuck::cast_slice::<_, u8>(&fresh.vertices));
|
||||
images.insert(bytes);
|
||||
}
|
||||
assert!(images.len() > 1, "the fixture must actually animate");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_tag_objects_follow_live_sprite_updates() {
|
||||
let mut atlas = HudAtlas::boot();
|
||||
let mut sprite = atlas.sprites["horizon/white"];
|
||||
atlas.sprites.insert("item/probe".into(), sprite);
|
||||
let text = horizon_client::component::to_text(
|
||||
&serde_json::json!({"object":"atlas", "sprite":"minecraft:item/probe"}),
|
||||
&mut |_| Text::empty(),
|
||||
);
|
||||
let mut cache = NameTagLayouts::default();
|
||||
let first = tag_bytes(&mut cache, &atlas, &text, 255, false);
|
||||
assert!(!first.is_empty());
|
||||
sprite.u0 += 0.1;
|
||||
atlas.sprites.insert("item/probe".into(), sprite);
|
||||
assert_ne!(tag_bytes(&mut cache, &atlas, &text, 255, false), first);
|
||||
}
|
||||
|
||||
/// The player model `perspective::player_type` builds from the prepared
|
||||
/// resources, without them: its box only needs the id and the pointer.
|
||||
fn player_mob() -> Mob {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"base": "2b74332fb",
|
||||
"binaries": {"baseline":"6997749681fa69634449dd688906200fbce8e748352962bb3f4ef8d9e311d1c3","candidate":"7ea4dc7f73e83c0980df8d0bad7081ffe0cae48935663689b5f31ee4b9d7ac0a"},
|
||||
"quality": [{"scene":"initial","changed_pixels":0},{"scene":"rename","changed_pixels":0},{"scene":"score_team","changed_pixels":0},{"scene":"move","changed_pixels":0},{"scene":"font_options","changed_pixels":0}],
|
||||
"samples": [
|
||||
{"cycle":0,"pair":0,"variant":"baseline","scene":"front","fps":1051.7966220784283,"cpu_ms":0.883081021403224,"gpu_ms":0.206471023594761,"prepare_ms":0.7426633483563068,"low_1pct_fps":353.7721737039538},
|
||||
{"cycle":0,"pair":0,"variant":"baseline","scene":"behind","fps":1058.3068019790742,"cpu_ms":0.8673469352249216,"gpu_ms":0.1895598093656891,"prepare_ms":0.7250369851121037,"low_1pct_fps":330.5956813188397},
|
||||
{"cycle":0,"pair":0,"variant":"baseline","scene":"empty","fps":4292.971633366724,"cpu_ms":0.09846154169018746,"gpu_ms":0.2041446953867861,"prepare_ms":0.09032092565794698,"low_1pct_fps":690.9605932265464},
|
||||
{"cycle":1,"pair":0,"variant":"candidate","scene":"front","fps":1791.2371609529432,"cpu_ms":0.4996271979017739,"gpu_ms":0.20476298797860049,"prepare_ms":0.36100063756483647,"low_1pct_fps":604.4971584684645},
|
||||
{"cycle":1,"pair":0,"variant":"candidate","scene":"behind","fps":2069.60366352984,"cpu_ms":0.42620941923499495,"gpu_ms":0.18699220776309555,"prepare_ms":0.27871038342104654,"low_1pct_fps":577.4636859002027},
|
||||
{"cycle":1,"pair":0,"variant":"candidate","scene":"empty","fps":4432.343371510415,"cpu_ms":0.10048993249552,"gpu_ms":0.19595694637201425,"prepare_ms":0.09090468674631716,"low_1pct_fps":694.9883639147564},
|
||||
{"cycle":2,"pair":1,"variant":"candidate","scene":"front","fps":1783.7215267668012,"cpu_ms":0.4981431789931168,"gpu_ms":0.2059234568286627,"prepare_ms":0.3616274793914923,"low_1pct_fps":576.8883860205156},
|
||||
{"cycle":2,"pair":1,"variant":"candidate","scene":"behind","fps":2120.353605541252,"cpu_ms":0.41953523691758543,"gpu_ms":0.1869485721587643,"prepare_ms":0.27214778206787477,"low_1pct_fps":652.8169593351126},
|
||||
{"cycle":2,"pair":1,"variant":"candidate","scene":"empty","fps":4352.362053494388,"cpu_ms":0.09945434413161312,"gpu_ms":0.20217791819652903,"prepare_ms":0.09065125569217614,"low_1pct_fps":694.8098453359895},
|
||||
{"cycle":3,"pair":1,"variant":"baseline","scene":"front","fps":1064.1458890478812,"cpu_ms":0.8740313865266264,"gpu_ms":0.2054239459276311,"prepare_ms":0.7312424486620036,"low_1pct_fps":381.13598484312047},
|
||||
{"cycle":3,"pair":1,"variant":"baseline","scene":"behind","fps":1021.3711707677252,"cpu_ms":0.9035914639109252,"gpu_ms":0.1912185329518226,"prepare_ms":0.755900545932957,"low_1pct_fps":311.87492774414187},
|
||||
{"cycle":3,"pair":1,"variant":"baseline","scene":"empty","fps":4293.604458624059,"cpu_ms":0.1007658596148652,"gpu_ms":0.1992767261892394,"prepare_ms":0.09101183841879054,"low_1pct_fps":613.41926178891},
|
||||
{"cycle":4,"pair":2,"variant":"candidate","scene":"front","fps":1708.9184872076082,"cpu_ms":0.5145837150904827,"gpu_ms":0.20600104381666184,"prepare_ms":0.3764380921278598,"low_1pct_fps":466.519853250343},
|
||||
{"cycle":4,"pair":2,"variant":"candidate","scene":"behind","fps":1971.3836386165208,"cpu_ms":0.4405532517455539,"gpu_ms":0.18877758733201755,"prepare_ms":0.29067299283630893,"low_1pct_fps":516.055351824928},
|
||||
{"cycle":4,"pair":2,"variant":"candidate","scene":"empty","fps":4039.969424796982,"cpu_ms":0.1029075857445524,"gpu_ms":0.20901548113134363,"prepare_ms":0.09179950450249662,"low_1pct_fps":584.4091087025574},
|
||||
{"cycle":5,"pair":2,"variant":"baseline","scene":"front","fps":1186.5142344638175,"cpu_ms":0.7795670113175297,"gpu_ms":0.2049857092726036,"prepare_ms":0.6376724056954481,"low_1pct_fps":483.5010731703909},
|
||||
{"cycle":5,"pair":2,"variant":"baseline","scene":"behind","fps":1285.7991642799304,"cpu_ms":0.7128664263254052,"gpu_ms":0.18684411437678083,"prepare_ms":0.568760129517643,"low_1pct_fps":479.14198527216934},
|
||||
{"cycle":5,"pair":2,"variant":"baseline","scene":"empty","fps":4345.309129680775,"cpu_ms":0.1030133317635822,"gpu_ms":0.1945606774064114,"prepare_ms":0.0914824880992008,"low_1pct_fps":611.6066635319316},
|
||||
{"cycle":6,"pair":3,"variant":"baseline","scene":"front","fps":1126.8745909323516,"cpu_ms":0.8168730579296783,"gpu_ms":0.20928317886721856,"prepare_ms":0.6715726909007027,"low_1pct_fps":402.68750812736306},
|
||||
{"cycle":6,"pair":3,"variant":"baseline","scene":"behind","fps":1328.1012170271017,"cpu_ms":0.6963829583336325,"gpu_ms":0.1864167663419948,"prepare_ms":0.5539404330337748,"low_1pct_fps":521.6764535451866},
|
||||
{"cycle":6,"pair":3,"variant":"baseline","scene":"empty","fps":4391.61504574128,"cpu_ms":0.10059389554385516,"gpu_ms":0.195875587951098,"prepare_ms":0.09059730297340954,"low_1pct_fps":632.1743658852401},
|
||||
{"cycle":7,"pair":3,"variant":"candidate","scene":"front","fps":1629.907260557841,"cpu_ms":0.5466166728409472,"gpu_ms":0.2040921990116931,"prepare_ms":0.4020578124224546,"low_1pct_fps":471.930871438953},
|
||||
{"cycle":7,"pair":3,"variant":"candidate","scene":"behind","fps":1898.8692399125523,"cpu_ms":0.4603509318971386,"gpu_ms":0.18630747471286943,"prepare_ms":0.3132662786677051,"low_1pct_fps":503.5847635901019},
|
||||
{"cycle":7,"pair":3,"variant":"candidate","scene":"empty","fps":4417.477650634551,"cpu_ms":0.10235459178565988,"gpu_ms":0.1942967347426569,"prepare_ms":0.09195623963719245,"low_1pct_fps":644.5895240254}
|
||||
],
|
||||
"summary": {"behind":{"variants":{"baseline":{"fps_median":1172.0529831295023,"cpu_ms_median":0.7901066807751633,"gpu_ms_median":0.18820196187123497,"prepare_ms_median":0.6468985573148733,"low_1pct_fps_median":404.8688332955045},"candidate":{"fps_median":2020.4936510731804,"cpu_ms_median":0.4333813354902744,"gpu_ms_median":0.1869703899609299,"prepare_ms_median":0.28469168812867773,"low_1pct_fps_median":546.7595188625653}},"paired_gain_pct":72.71917187176544,"ci95_approx_pct":[29.334446655428092,130.65713043599604],"pixels":[{"pair":0,"different_pixels":0,"max_channel_diff":0},{"pair":1,"different_pixels":0,"max_channel_diff":0},{"pair":2,"different_pixels":0,"max_channel_diff":0},{"pair":3,"different_pixels":0,"max_channel_diff":0}]},"empty":{"variants":{"baseline":{"fps_median":4319.4567941524165,"cpu_ms_median":0.10067987757936017,"gpu_ms_median":0.19757615707016868,"prepare_ms_median":0.09080457069610004,"low_1pct_fps_median":622.7968138370751},"candidate":{"fps_median":4384.9198520644695,"cpu_ms_median":0.10142226214058994,"gpu_ms_median":0.19906743228427165,"prepare_ms_median":0.09135209562440688,"low_1pct_fps_median":669.6996846806948}},"paired_gain_pct":-0.5347303327741403,"ci95_approx_pct":[-7.599617963294701,7.070335120951254],"pixels":[{"pair":0,"different_pixels":0,"max_channel_diff":0},{"pair":1,"different_pixels":0,"max_channel_diff":0},{"pair":2,"different_pixels":0,"max_channel_diff":0},{"pair":3,"different_pixels":0,"max_channel_diff":0}]},"front":{"variants":{"baseline":{"fps_median":1095.5102399901164,"cpu_ms_median":0.8454522222281524,"gpu_ms_median":0.20594748476119606,"prepare_ms_median":0.7014075697813531,"low_1pct_fps_median":391.9117464852418},"candidate":{"fps_median":1746.3200069872046,"cpu_ms_median":0.5071054564961284,"gpu_ms_median":0.2053432224036316,"prepare_ms_median":0.369032785759676,"low_1pct_fps_median":524.4096287297343}},"paired_gain_pct":56.16033284896831,"ci95_approx_pct":[35.07186954292121,80.54128989272277],"pixels":[{"pair":0,"different_pixels":1377,"max_channel_diff":190},{"pair":1,"different_pixels":0,"max_channel_diff":0},{"pair":2,"different_pixels":1377,"max_channel_diff":190},{"pair":3,"different_pixels":1377,"max_channel_diff":190}]}}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
# Entity name-tag layout cache
|
||||
|
||||
Unchanged entity names used to repeat font width, style/bidi processing and glyph expansion
|
||||
on every frame, separately for the normal and see-through passes. Retain only their local
|
||||
HUD quads. Text/style, pass alpha/background, vertical text offset, atlas revision, font epoch
|
||||
and bidi direction determine reuse. World placement, camera orientation, lighting, cosmetic
|
||||
marks and visibility/hooks still update each frame. This does not change TextDisplay's existing
|
||||
line cache or the player-list/skin paths.
|
||||
|
||||
Obfuscated text (including legacy §k) and inline objects always use the original preparation.
|
||||
Keep at most 512 lines, each at most 512 HUD vertices and 1024 text bytes; larger text and
|
||||
excess lines still draw through the original path. Slots follow submission order and compare
|
||||
complete styled text, so reordered/removed entities, scores and teams cannot reuse another
|
||||
name. Unused slots are dropped at the frame end. No quality setting is needed.
|
||||
|
||||
## Measurement
|
||||
|
||||
Base `2b74332fb`, Linux/Vulkan/X11, Ryzen 5 7500F, RTX 4060, 1920×1080,
|
||||
render distance 4, VSync off/unlimited FPS, OIT off. Fresh isolated homes and game folders,
|
||||
same copied save, frozen server ticks, fixed time/weather and camera. No other game or
|
||||
build ran during the measurements; existing desktop apps stayed running and power settings
|
||||
were unchanged. A/B/B/A/B/A/A/B launch order, 3 s warmup then 8 s measured per scene:
|
||||
four pairs, 24 samples including the empty control. This isolates rendering of static names;
|
||||
it is not a general gameplay or low-FPS/GPU-bound benchmark.
|
||||
|
||||
100 invisible marker armor stands, unique gold bold `Rank NNN Player` custom names,
|
||||
10×10 grid with 1.5-block spacing at y=81. Camera feet (0.5,80,-9.5), pitch 8°;
|
||||
yaw 0° faces them, yaw 180° faces away. Chat visibility is 2. Normal HUD remains enabled,
|
||||
since hiding it also hides name tags. The empty control removes only entity regions from
|
||||
a scratch copy of the same save. Values below are medians of four runs.
|
||||
|
||||
| Scene | Baseline FPS | Cached FPS | Paired gain (approx. 95% t interval) | Prepare ms, before → after |
|
||||
|---|---:|---:|---:|---:|
|
||||
| front | 1096 | 1746 | +56.2% [+35.1, +80.5] | 0.701 → 0.369 |
|
||||
| behind | 1172 | 2020 | +72.7% [+29.3, +130.7] | 0.647 → 0.285 |
|
||||
| empty | 4319 | 4385 | -0.5% [-7.6, +7.1] | 0.091 → 0.091 |
|
||||
|
||||
The interval uses paired log FPS ratios (four pairs, t critical 3.182), not individual frames
|
||||
as independent samples. The empty control does not establish an FPS improvement. Gains
|
||||
apply to many unchanged names; changing/animated text and GPU-bound scenes may benefit less.
|
||||
[Raw samples and summaries](name-tag-layout-cache-samples.json) include CPU/GPU and 1% lows.
|
||||
|
||||
## Correctness
|
||||
|
||||
Five independent 1280×720 image comparisons against the baseline had zero changed pixels:
|
||||
plain/styled/legacy/Arabic names with below-name scores and a team; live rename/style change;
|
||||
score/team update; camera translation; force-Unicode font option. These were four separated
|
||||
invisible marker stands, not a pile of overlapping models. Unit tests additionally compare
|
||||
cached/fresh vertex bytes after text/style/pass changes and a font reset that moves glyph UVs,
|
||||
check that §k keeps animating with seeded fresh-path comparisons, and update a live inline
|
||||
sprite without replacing the atlas. The ordinary rendering path and its visibility remain
|
||||
the reference; this changes preparation reuse, not vanilla name rules.
|
||||
|
||||
The dense front-view stress fixture has two draw orders when entity chunks arrive in a
|
||||
different order. Three paired images differ in 1377 pixels where names overlap. An additional
|
||||
unmodified-baseline launch reproduced the candidate's alternate image exactly (0 differences),
|
||||
while two baseline launches reproduced the first order. This is existing overlap compositing,
|
||||
not a pixel oracle; the five separated-name scenes above avoid it.
|
||||
Reference in new issue
Block a user