Respect compiled standard-pack entity shadow ownership

Use the running pack's compiled non-player entity caster capability to
avoid adding vanilla mob blobs beside its shadows. Preserve fallback
for player-only, disabled, absent and failed paths and native dimensions.

Add original compiled-pipeline and production sprite-selection controls.

Co-Authored-By: Codex GPT-6.1-sol <[email protected]>
This commit is contained in:
gigamans442andCodex GPT-6.1-sol committed 2026-11-02 15:34:50 -05:00
1 parent bc761767d3
commit d3117ec15d
5 files changed
+397 -2

No files matched your search

@@ -0,0 +1,220 @@
//! Original synthetic capability regression; no game resources or player world.
//! Run with fresh HORIZON_HOME, HORIZON_LAUNCHER_HOME, XDG_DATA_HOME and XDG_CONFIG_HOME,
//! HORIZON_NO_AUDIO=1 and HORIZON_LAUNCHER_NO_SELF_UPDATE=1.
use horizon_client_render::{gfx::Renderer, graphics::GraphicsApi};
use std::{io::Write, sync::Arc};
use winit::{
application::ApplicationHandler,
event::WindowEvent,
event_loop::{ActiveEventLoop, EventLoop},
window::{Window, WindowId},
};
const VERTEX: &str = "#version 120\nvoid main() { gl_Position = ftransform(); }\n";
const COLOUR: &str = "#version 120\nvoid main() { gl_FragColor = vec4(1.0); }\n";
// Reading the actual shadow sampler activates the shadow pass, rather than just
// placing a shadow program in the archive and assuming the pass runs.
const SHADOW_READ: &str = "#version 120\nuniform sampler2D shadowtex0;\n\
void main() { gl_FragColor = vec4(vec3(texture2D(shadowtex0, vec2(0.5)).r), 1.0); }\n";
fn pack(properties: &str, shadow: Option<&str>, reads_shadow: bool) -> horizon_shaders::Pack {
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
let mut write = |name: &str, text: &str| {
zip.start_file(
format!("shaders/{name}"),
zip::write::SimpleFileOptions::default(),
)
.unwrap();
zip.write_all(text.as_bytes()).unwrap();
};
write("shaders.properties", properties);
write("final.vsh", VERTEX);
write("final.fsh", if reads_shadow { SHADOW_READ } else { COLOUR });
write("gbuffers_entities.vsh", VERTEX);
write("gbuffers_entities.fsh", COLOUR);
if let Some(shadow) = shadow {
write("shadow.vsh", VERTEX);
write("shadow.fsh", shadow);
}
horizon_shaders::Pack::from_zip_bytes(
"original-shadow-ownership",
zip.finish().unwrap().into_inner(),
)
.unwrap()
}
fn fixture(window: Arc<Window>) -> Result<(), Box<dyn std::error::Error>> {
let white = [255; 4];
let panorama = Arc::new(horizon_client_render::gfx::panorama::Faces::colour([0; 4]));
let mut renderer = pollster::block_on(Renderer::new(
window,
true,
&white,
1,
1,
1,
&[],
&[],
(&white, 1, 1),
(&white, 1, 1),
(&white, 1, 1),
(&white, 1),
panorama,
None,
None,
GraphicsApi::Vulkan,
false,
true,
))?;
assert!(!renderer.pack_entity_shadows(), "no selected pack");
renderer.set_shader_pack(0, None, &[]);
assert!(
!renderer.pack_entity_shadows(),
"selected pack without a compiled post"
);
let cases = [
("full", "", Some(COLOUR), true, false, true),
(
"player-only",
"shadowEntities=false\nshadowPlayer=true",
Some(COLOUR),
true,
false,
false,
),
// Block-entity shadows alone must not suppress a mob's blob.
(
"blocks-only",
"shadowEntities=false",
Some(COLOUR),
true,
false,
false,
),
(
"disabled",
"shadow.enabled=false",
Some(COLOUR),
true,
false,
false,
),
("missing-program", "", None, true, false, false),
(
"failed-translation",
"",
Some("#version 120\nvoid main() { gl_FragColor = deliberately_undefined; }"),
true,
false,
false,
),
("failed-pipeline", "", Some(COLOUR), true, true, false),
(
"unused-shadow-program",
"",
Some(COLOUR),
false,
false,
false,
),
(
"forced-shadow-program",
"shadow.enabled=true",
Some(COLOUR),
false,
false,
true,
),
];
for dimension in [0, -1, 1] {
for (name, properties, shadow, reads_shadow, break_pipeline, expected) in cases {
let pack = pack(properties, shadow, reads_shadow);
let mut macros = horizon_shaders::Macros::standard();
renderer.define_pack_macros(&mut macros);
let (properties, _) = horizon_shaders::read_properties(&pack, &mut macros);
let mut post = horizon_shaders::load_post(
&pack,
"",
&properties,
&macros,
&renderer.pack_inputs(),
);
if name == "failed-translation" {
assert!(post.shadow_entities.is_none());
assert!(post.notes.iter().any(|note| note.contains("skipped")));
} else if name == "missing-program" {
assert!(post.shadow_entities.is_none());
assert!(post.notes.iter().any(|note| note.contains("no program")));
} else if break_pipeline {
// A translated stage can still fail device pipeline creation. Inject a
// missing entry point into only the entity caster; terrain/block stages
// and the settings stay valid. PackPost must report the compiled result.
post.shadow_entities
.as_mut()
.unwrap()
.program
.as_mut()
.unwrap()
.vertex
.entry_points[0]
.name = "deliberately_missing_main".into();
} else {
assert!(post.notes.is_empty(), "{name}: {:?}", post.notes);
}
let (_, notes) = renderer
.set_shader_pack(dimension, Some(post), &[])
.unwrap();
if break_pipeline {
assert!(
notes.iter().any(|note| note.contains("skipped")),
"{notes:?}"
);
} else if name != "missing-program" && name != "failed-translation" {
assert!(
!notes.iter().any(|note| note.contains("skipped")),
"{name}: {notes:?}"
);
}
assert_eq!(
renderer.pack_entity_shadows(),
expected,
"{name}, dimension {dimension}: {notes:?}"
);
assert!(!renderer.native_shadow());
println!("{name}, dimension {dimension}: replaces_mob_blob={expected}");
}
}
renderer.drop_shader_packs();
assert!(
!renderer.pack_entity_shadows(),
"dropped pack restores fallback"
);
println!("ENTITY_SHADOW_OWNERSHIP_PASSED: 27 compiled controls and no/pending/dropped pack");
Ok(())
}
struct App;
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let window = Arc::new(
event_loop
.create_window(
Window::default_attributes()
.with_visible(false)
.with_inner_size(winit::dpi::LogicalSize::new(32., 32.)),
)
.unwrap(),
);
if let Err(error) = fixture(window) {
eprintln!("Shadow ownership fixture failed: {error}");
std::process::exit(1);
}
event_loop.exit();
}
fn window_event(&mut self, _: &ActiveEventLoop, _: WindowId, _: WindowEvent) {}
}
fn main() {
EventLoop::new().unwrap().run_app(&mut App).unwrap();
}
+12
View File
@@ -4422,6 +4422,18 @@ impl Renderer {
((c.entities && !c.only_player) || c.blocks, c.entities)
}
/// A standard pack replaces mobs' vanilla blob shadows only when its compiled
/// shadow path actually draws entities other than the player.
pub fn pack_entity_shadows(&self) -> bool {
self.pack_post
.as_ref()
.and_then(|(_, post)| post.as_ref())
.is_some_and(|post| {
let casters = post.shadow_casters();
casters.entities && !casters.only_player
})
}
pub fn has_section(&self, key: SectionKey) -> bool {
self.sections.contains(key)
}
+3 -2
View File
@@ -10620,8 +10620,9 @@ impl Game<'_> {
self.mark_translucent_entity(out.len(), "minecraft:entity_shadow");
// A native shader pack's sun shadows already fall under the entities in the Overworld:
// the round blob would be a second one. The Nether and the End have no sun to cast them.
let pack_shadows =
self.dim == Dim::Overworld && self.renderer.as_ref().is_some_and(|r| r.native_shadow());
let pack_shadows = self.renderer.as_ref().is_some_and(|r| {
r.pack_entity_shadows() || (self.dim == Dim::Overworld && r.native_shadow())
});
let shadow_sprite = (self.settings.entity_shadows && !pack_shadows)
.then(|| self.gui.atlas.sprites.get("misc/shadow").copied())
.flatten();
+26
View File
@@ -0,0 +1,26 @@
"""Game blob fallback through the actual production selection/caster methods."""
from pathlib import Path
import shutil
import subprocess
import sys
import unittest
ROOT = Path(__file__).resolve().parents[1]
class EntityShadowOwnership(unittest.TestCase):
@unittest.skipUnless(shutil.which("rustc"), "requires Rust to execute production methods")
def test_compiled_caster_paths_select_the_blob_fallback(self):
result = subprocess.run(
[sys.executable, str(ROOT / "tools/test_entity_shadow_ownership.py")],
cwd=ROOT,
capture_output=True,
text=True,
timeout=60,
)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
if __name__ == "__main__":
unittest.main()
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""Execute the production blob selection and compiled-caster methods in isolation.
This does not assert source spelling or duplicate the decision under test. The
Rust statements are extracted unchanged; stand-ins supply the surrounding GPU
handles and atlas. The companion entity_shadow_ownership example builds actual
pipelines. Run this against an unfixed checkout with --source-root for the
before control. No game, assets, launcher or player data are used.
"""
import argparse
from pathlib import Path
import subprocess
import tempfile
def method(source, name):
start = source.index(f"pub fn {name}(")
opening = source.index("{", start)
depth = 1
end = opening + 1
while depth:
depth += (source[end] == "{") - (source[end] == "}")
end += 1
return source[start:end]
def program(root):
gfx = (root / "crates/horizon-client-render/src/gfx.rs").read_text()
post = (root / "crates/horizon-client-render/src/gfx/pack_post.rs").read_text()
game = (root / "crates/horizon-client/src/game.rs").read_text()
geometry = game.index("fn entity_geometry(")
start = game.index("let pack_shadows =", geometry)
end = game.index("if let Some(sprite) = shadow_sprite", start)
guard = game[start:end]
getter = method(gfx, "pack_entity_shadows") if "pub fn pack_entity_shadows(" in gfx else ""
support = r'''
#![allow(dead_code)]
mod pack_entities {
#[derive(Clone, Copy, Default)]
pub struct Casters {
pub entities: bool, pub only_player: bool, pub blocks: bool, pub only_lit: bool,
}
}
mod post {
pub struct Entities { pub shadow: [Option<()>; 2], pub only: (bool, bool) }
pub struct PackPost { pub entities: Option<Entities>, pub shadow_targets: Option<()> }
impl PackPost { __CASTERS__ }
}
struct Native { shadow: Option<()> }
struct Renderer { pack_post: Option<(i32, Option<post::PackPost>)>, native: Option<Native> }
impl Renderer { __NATIVE__ __GETTER__ }
#[derive(Clone, Copy, PartialEq)]
enum Dim { Overworld, Nether, End }
struct Settings { entity_shadows: bool }
struct Atlas { sprites: std::collections::HashMap<&'static str, usize> }
struct Gui { atlas: Atlas }
struct Game { dim: Dim, renderer: Option<Renderer>, settings: Settings, gui: Gui }
impl Game {
fn selected_blob(&self) -> Option<usize> { __GUARD__ shadow_sprite }
}
fn scene(kind: &str, dim: Dim, enabled: bool, sprite: bool) -> Game {
let casters = |entities: bool, only_player: bool, blocks: bool, targets: bool| {
Some((0, Some(post::PackPost {
entities: Some(post::Entities {
shadow: [entities.then_some(()), blocks.then_some(())], only: (only_player, false),
}),
shadow_targets: targets.then_some(()),
})))
};
let pack_post = match kind {
"full" => casters(true, false, true, true),
"player_only" => casters(true, true, true, true),
"blocks_only" => casters(false, false, true, true),
"failed_pipeline" => casters(false, false, false, true),
"disabled_targets" => casters(true, false, true, false),
"missing_entities" => Some((0, Some(post::PackPost { entities: None, shadow_targets: Some(()) }))),
"pending" => Some((0, None)),
_ => None,
};
Game {
dim,
renderer: (kind != "no_renderer").then_some(Renderer {
pack_post,
native: (kind == "native").then_some(Native { shadow: Some(()) }),
}),
settings: Settings { entity_shadows: enabled },
gui: Gui { atlas: Atlas { sprites: if sprite {
[("misc/shadow", 73)].into_iter().collect()
} else { Default::default() } } },
}
}
'''
source = (support.replace("__CASTERS__", method(post, "shadow_casters"))
.replace("__NATIVE__", method(gfx, "native_shadow"))
.replace("__GETTER__", getter).replace("__GUARD__", guard))
# Independent expected outcomes: a full standard entity caster owns the
# mob shadow in each dimension. Native shadows own it only in the Overworld.
rows = [
("full", [None, None, None]),
("player_only", [73, 73, 73]),
("blocks_only", [73, 73, 73]),
("failed_pipeline", [73, 73, 73]),
("disabled_targets", [73, 73, 73]),
("missing_entities", [73, 73, 73]),
("pending", [73, 73, 73]),
("no_pack", [73, 73, 73]),
("no_renderer", [73, 73, 73]),
("native", [None, 73, 73]),
]
for kind, expected in rows:
for dimension, wanted in zip(["Overworld", "Nether", "End"], expected):
result = "None" if wanted is None else f"Some({wanted})"
source += f'''
#[test] fn {kind}_{dimension.lower()}() {{
assert_eq!(scene("{kind}", Dim::{dimension}, true, true).selected_blob(), {result});
assert_eq!(scene("{kind}", Dim::{dimension}, false, true).selected_blob(), None);
assert_eq!(scene("{kind}", Dim::{dimension}, true, false).selected_blob(), None);
}}
'''
return source
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--source-root", type=Path, default=Path(__file__).resolve().parents[1])
args = parser.parse_args()
with tempfile.TemporaryDirectory(prefix="horizon-shadow-ownership-") as folder:
source = Path(folder) / "ownership.rs"
source.write_text(program(args.source_root))
binary = Path(folder) / "ownership"
subprocess.run(["rustc", "--edition=2024", "--test", str(source), "-o", str(binary)], check=True)
return subprocess.run([str(binary), "--nocapture"]).returncode
if __name__ == "__main__":
raise SystemExit(main())