From 2ef8b7b397e7ac34361ec69d3dca57a3fba8d3b4 Mon Sep 17 00:00:00 2001 From: AyeZeeBB Date: Sun, 25 Oct 2026 14:50:36 -0500 Subject: [PATCH] Shaders: of two usable custom graphs under an alias-only winner, the higher-priority pack's is kept The promotion loop went through the edits in the order they were absorbed, lowest priority first, and kept the first usable graph, so a lower pack's graph won and the higher pack's private rows were dropped. It now looks from the highest priority down. The archive test covers two custom packs with their own bytecode and different second programs (it runs with a game root, as the other archive checks do). --- .../Vfs/Test/mod_merge_added_assets_tests.cpp | 25 +++++++++++++++---- Engine/Vfs/mod_merge_combine.cpp | 4 ++- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/Engine/Vfs/Test/mod_merge_added_assets_tests.cpp b/Engine/Vfs/Test/mod_merge_added_assets_tests.cpp index 7583ffa8..2a2356ac 100644 --- a/Engine/Vfs/Test/mod_merge_added_assets_tests.cpp +++ b/Engine/Vfs/Test/mod_merge_added_assets_tests.cpp @@ -1029,10 +1029,12 @@ void catalog_checks_native_cooperation_again_when_the_variant_changes() { "two private contributors reusing a GUID are both refused in either priority order"); } } +// `second_custom`: a higher-priority custom pack too, with its own bytecode, so both custom graphs can +// be kept; the higher one's must be. void shader_selection_checks_the_winner(bool delivered, bool changed_heap, bool alias_first, bool overwritten = false, - bool encoder_available = true) { + bool encoder_available = true, bool second_custom = false) { Fixture fixture("shader-selection-" + std::to_string(delivered) + std::to_string(changed_heap) + - std::to_string(alias_first) + std::to_string(overwritten)); + std::to_string(alias_first) + std::to_string(overwritten) + std::to_string(second_custom)); const auto table = [](bool extended) { Resource resource{"test/program_lookup", Bytes(extended ? 544 : 208)}; const std::uint32_t count = extended ? 2 : 1; @@ -1055,7 +1057,10 @@ void shader_selection_checks_the_winner(bool delivered, bool changed_heap, bool auto aliases = rows(table(false), {{1, 0}, {2, 0}}); auto custom = rows(table(true), {{1, 0}, {2, 0}, {5, 0}, {9, 1}, {99, 2}}); if (changed_heap) custom.payload[192] = std::byte{8}; - const auto write_layer = [&](const fs::path& root, bool patch, const Resource& programs, bool code, unsigned code_version) { + auto higher = rows(table(true), {{1, 0}, {2, 0}, {11, 1}}); + higher.payload[400] = std::byte{3}; // its own second program: another graph than the lower pack's + const auto write_layer = [&](const fs::path& root, bool patch, const Resource& programs, bool code, unsigned code_version, + const char* code_name = "shaders/bytecode/test", std::uint32_t code_id = 43) { fs::create_directories(root / "Win32" / "pkg"); write(root / "layout.toc", layout_toc()); fb::BinaryBundle manifest; @@ -1069,7 +1074,7 @@ void shader_selection_checks_the_winner(bool delivered, bool changed_heap, bool manifest.resources.push_back(asset); const Bytes bytecode{static_cast(code_version)}; if (code) { - asset.name = "shaders/bytecode/test"; asset.resourceId = 43; asset.resourceType = fb::shader::bytecodeType; + asset.name = code_name; asset.resourceId = code_id; asset.resourceType = fb::shader::bytecodeType; asset.sha1 = mods::detail::sha1_of(bytecode); asset.originalSize = bytecode.size(); asset.resourceMeta.assign(16, std::byte{0}); manifest.resources.push_back(asset); } @@ -1089,7 +1094,11 @@ void shader_selection_checks_the_winner(bool delivered, bool changed_heap, bool write_layer(fixture.catalog.data_root / "Data", false, stock, false, 0); write_layer(fixture.catalog.root / "custom", true, custom, delivered, 1); write_layer(fixture.catalog.root / "aliases", true, aliases, overwritten, 2); - for (const auto* name : alias_first ? std::vector{"aliases", "custom"} : std::vector{"custom", "aliases"}) { + if (second_custom) write_layer(fixture.catalog.root / "higher", true, higher, true, 3, "shaders/bytecode/higher", 44); + // The first mod listed has the highest priority. + auto order = alias_first ? std::vector{"aliases", "custom"} : std::vector{"custom", "aliases"}; + if (second_custom) order = {"aliases", "higher", "custom"}; + for (const auto* name : order) { mods::Mod mod; mod.name = name; mod.directory = fixture.catalog.root / name; mod.provides_layout = true; fixture.catalog.mods.push_back(std::move(mod)); } @@ -1139,6 +1148,11 @@ void shader_selection_checks_the_winner(bool delivered, bool changed_heap, bool expect(index < count, "native output rows stay in the retained graph's range"); } } + if (second_custom) { + expect(count == 2 && keys.contains(11) && !keys.contains(9), + "of two usable custom graphs under an alias-only winner, the higher-priority one is kept"); + return; + } expect(keys.contains(2) && !keys.contains(99), "safe stock aliases survive and invalid winning rows are actually written out"); expect(keys.contains(9) == private_graph, "unproved graphs cannot leak private program indices into stock"); expect(keys.contains(5) == !changed_heap, "a non-retained custom contributor still contributes a proven stock alias"); @@ -1155,6 +1169,7 @@ int main(int argc, char** argv) try { shader_selection_checks_the_winner(false, false, false); shader_selection_checks_the_winner(true, true, false); shader_selection_checks_the_winner(true, false, true, true); + shader_selection_checks_the_winner(true, false, true, false, true, true); } else { shader_selection_checks_the_winner(true, false, true, false, false); std::cout << "Successful shader archive selection checks require a local game root; skipped\n"; diff --git a/Engine/Vfs/mod_merge_combine.cpp b/Engine/Vfs/mod_merge_combine.cpp index fc9e5080..717ec43c 100644 --- a/Engine/Vfs/mod_merge_combine.cpp +++ b/Engine/Vfs/mod_merge_combine.cpp @@ -1224,7 +1224,9 @@ fb::TocDocument combine(const fs::path& baseToc, const fs::path& baseRoot, retainedBytes = baseBytes; } if (fb::shader::only_stock_aliases({retainedBytes, retained->resourceMeta}, stockTable)) { - for (std::size_t i = 0; i < edits.size(); ++i) { + // `edits` is lowest priority first, as the copies were absorbed: the + // highest-priority usable graph is the one kept. + for (std::size_t i = edits.size(); i-- > 0;) { const fb::shader::Table candidate{editBytes[i], edits[i]->resourceMeta}; if (fb::shader::same_program_graph(candidate, stockTable) || !usable(candidate, edits[i])) continue; retained = edits[i];