mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-26 15:06:42 +01:00
Shaders: validate retained graphs and preserve safe aliases
This commit is contained in:
1 parent
dd74399e8a
commit
8f3611e283
6 files changed
+243
-42
No files matched your search
@@ -146,6 +146,11 @@ int main() {
|
||||
auto shortMeta = extension;
|
||||
shortMeta.meta.resize(8);
|
||||
check(!fb::shader::preserves_stock_programs(shortMeta.table(), stock.table()), "Short retained metadata is refused");
|
||||
auto inflatedCount = extension;
|
||||
const std::uint32_t impossibleCount = 0xffffffffU;
|
||||
std::memcpy(inflatedCount.resource.data(), &impossibleCount, sizeof(impossibleCount));
|
||||
check(!fb::shader::preserves_stock_programs(inflatedCount.table(), stock.table()),
|
||||
"A declared private program count must fit inside the retained heap");
|
||||
#ifdef _WIN32
|
||||
SYSTEM_INFO system{};
|
||||
GetSystemInfo(&system);
|
||||
@@ -199,6 +204,35 @@ int main() {
|
||||
"Stock aliases cannot address the selected graph's private program slots");
|
||||
check(fb::shader::merge_program_lookup(extension.table(), stockEdits).outside == 1,
|
||||
"The stock exception requires the caller's actual game table");
|
||||
auto otherCustom = preserved_lookup(true, {{1, {0}}, {12, {0}}, {13, {1}}});
|
||||
otherCustom.resource[400] = std::byte{9}; // a different private record, leaving stock intact
|
||||
const std::vector<fb::shader::Table> competing{otherCustom.table()};
|
||||
const auto sharedAliases = fb::shader::merge_program_lookup(extension.table(), competing, stock.table());
|
||||
check(sharedAliases.added == 1 && sharedAliases.outside == 1 && rows_of(sharedAliases).contains(12) &&
|
||||
!rows_of(sharedAliases).contains(13), "A non-retained custom graph contributes proven stock aliases, never its private indices");
|
||||
const auto stockRetained = fb::shader::merge_program_lookup(stock.table(), competing, stock.table());
|
||||
check(rows_of(stockRetained).contains(12) && !rows_of(stockRetained).contains(13),
|
||||
"Stock aliases from an extended graph survive when stock is retained too");
|
||||
const auto clash = preserved_lookup(false, {{1, {0}}, {9, {0}}});
|
||||
const std::vector<fb::shader::Table> clashing{clash.table()};
|
||||
const auto graphOwnerWins = fb::shader::merge_program_lookup(extension.table(), clashing, stock.table());
|
||||
check(graphOwnerWins.conflicts == 1 && graphOwnerWins.added == 0 &&
|
||||
rows_of(graphOwnerWins).at(9) == std::vector<std::uint32_t>{1},
|
||||
"Retained graph rows win clashes with a stock-alias winner, and the disagreement is reported");
|
||||
const auto sameGraphClash = lookup(5, {{1, {0}}, {9, {3}}});
|
||||
const std::vector<fb::shader::Table> graphClashes{sameGraphClash.table()};
|
||||
const auto selectedWins = fb::shader::merge_program_lookup(selected.table(), graphClashes);
|
||||
check(selectedWins.conflicts == 1 && rows_of(selectedWins).at(9) == std::vector<std::uint32_t>{4},
|
||||
"The selected contributor wins a same-graph clash with a lower-priority edit");
|
||||
const auto invalidWinner = lookup(5, {{1, {0}}, {99, {5}}});
|
||||
const auto filtered = fb::shader::merge_program_lookup(invalidWinner.table(), {});
|
||||
check(filtered.removed == 1 && filtered.outside == 1 && !rows_of(filtered).contains(99),
|
||||
"The retained winner's rows must be inside its own program count");
|
||||
auto corruptEdit = extension;
|
||||
corruptEdit.resource.resize(12);
|
||||
const std::vector<fb::shader::Table> corrupt{corruptEdit.table()};
|
||||
const auto kept = fb::shader::merge_program_lookup(stock.table(), corrupt, stock.table());
|
||||
check(kept.outside == 1 && rows_of(kept).contains(1), "Malformed edits cannot discard an intact retained table");
|
||||
if (failures) return 1;
|
||||
std::cout << "shader lookup tests passed\n";
|
||||
return 0;
|
||||
|
||||
@@ -73,6 +73,7 @@ bool preserves_stock(const Table& retained, const Table& stock) {
|
||||
if (!count || count > keptCount || stockSize < 12 || keptSize < stockSize ||
|
||||
start < 12 || start > stockSize || recordBytes > stockSize - start ||
|
||||
keptStart < 12 || keptStart > keptSize || recordBytes > keptSize - keptStart ||
|
||||
static_cast<std::uint64_t>(keptCount) * 168 > keptSize - keptStart ||
|
||||
!std::equal(stock.resourceMeta.begin() + 12, stock.resourceMeta.end(), retained.resourceMeta.begin() + 12) ||
|
||||
!std::equal(stock.resource.begin() + 12, stock.resource.begin() + stockSize, retained.resource.begin() + 12) ||
|
||||
!std::equal(stock.resource.begin() + static_cast<std::ptrdiff_t>(start),
|
||||
@@ -166,6 +167,10 @@ TextureRows read_texture_rows(const Table& table) {
|
||||
bool preserves_stock_programs(const Table& candidate, const Table& stock) {
|
||||
return preserves_stock(candidate, stock);
|
||||
}
|
||||
bool same_program_graph(const Table& candidate, const Table& other) {
|
||||
try { return same_graph(candidate, other); }
|
||||
catch (const std::exception&) { return false; }
|
||||
}
|
||||
|
||||
bool only_stock_aliases(const Table& candidate, const Table& stock) {
|
||||
try {
|
||||
@@ -194,22 +199,37 @@ MergedTable merge_program_lookup(const Table& base, const std::span<const Table>
|
||||
const bool stockPreserved = preserves_stock(base, stock);
|
||||
const auto stockRecords = stockPreserved ? read<std::uint32_t>(stock.resource, 0) : 0;
|
||||
MergedTable merged;
|
||||
for (auto at = rows.begin(); at != rows.end();) {
|
||||
if (std::any_of(at->second.begin(), at->second.end(), [&](auto index) { return index >= records; })) {
|
||||
at = rows.erase(at);
|
||||
++merged.outside;
|
||||
++merged.removed;
|
||||
} else ++at;
|
||||
}
|
||||
for (const auto& edit : edits) {
|
||||
// Indices have meaning only within their owning graph. Keep the selected
|
||||
// bundle's heap and relocations verbatim: importing another graph can
|
||||
// make the game instantiate programs before their compiled files load.
|
||||
const bool sameGraph = same_graph(base, edit);
|
||||
const bool stockGraph = stockPreserved && same_graph(stock, edit);
|
||||
const auto available = sameGraph ? records : stockRecords;
|
||||
for (const auto& [key, values] : read_program_rows(edit)) {
|
||||
if (baseRows.contains(key)) continue;
|
||||
if ((!sameGraph && !stockGraph) || std::any_of(values.begin(), values.end(), [&](std::uint32_t index) { return index >= available; })) {
|
||||
++merged.outside;
|
||||
continue;
|
||||
try {
|
||||
// Indices have meaning only within their owning graph. Keep the selected
|
||||
// bundle's heap and relocations verbatim: importing another graph can
|
||||
// make the game instantiate programs before their compiled files load.
|
||||
const bool sameGraph = same_graph(base, edit);
|
||||
const bool stockGraph = stockPreserved && (same_graph(stock, edit) || preserves_stock(edit, stock));
|
||||
const auto available = sameGraph ? records : stockRecords;
|
||||
for (const auto& [key, values] : read_program_rows(edit)) {
|
||||
if (const auto held = baseRows.find(key); held != baseRows.end()) {
|
||||
if (held->second != values) ++merged.conflicts;
|
||||
continue;
|
||||
}
|
||||
if ((!sameGraph && !stockGraph) || std::any_of(values.begin(), values.end(), [&](std::uint32_t index) { return index >= available; })) {
|
||||
++merged.outside;
|
||||
continue;
|
||||
}
|
||||
const auto [row, inserted] = rows.try_emplace(key, values);
|
||||
if (inserted) ++merged.added;
|
||||
else if (row->second != values) ++merged.conflicts;
|
||||
}
|
||||
const auto [row, inserted] = rows.try_emplace(key, values);
|
||||
if (inserted) ++merged.added;
|
||||
else if (row->second != values) ++merged.conflicts;
|
||||
} catch (const std::exception&) {
|
||||
// A malformed edit is not permission to discard a verified graph.
|
||||
++merged.outside;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ namespace dingosdk::frostbite::shader {
|
||||
|
||||
inline constexpr std::uint32_t programLookupType = 0xE2E6955B;
|
||||
inline constexpr std::uint32_t textureLookupType = 0x2D254A89;
|
||||
// Native ShaderProgram bytecode resource type, also used by Studio's native
|
||||
// shader resource writer; confirmed on the pinned build's shaders/bytecode/*.
|
||||
inline constexpr std::uint32_t bytecodeType = 0x0E2B02F7;
|
||||
|
||||
struct Table {
|
||||
@@ -25,18 +27,22 @@ struct MergedTable {
|
||||
std::vector<std::byte> resource;
|
||||
std::vector<std::byte> resourceMeta;
|
||||
std::size_t added{}; // rows taken from the edits
|
||||
std::size_t conflicts{}; // rows two edits disagreed on; the first won
|
||||
std::size_t conflicts{}; // disagreements: retained graph wins, then first edit
|
||||
std::size_t outside{}; // rows left out: they point past the base's programs
|
||||
std::size_t removed{}; // invalid rows removed from the retained table itself
|
||||
};
|
||||
|
||||
// The selected bundle contributor owns the retained program graph. Merge only
|
||||
// aliases whose program heap and relocation bytes match it exactly. No program
|
||||
// private aliases whose program heap and relocation bytes match it exactly. No program
|
||||
// records or relocations are copied or rebased. An optional stock table allows
|
||||
// stock aliases when its heap, records and relocations survive unchanged. `outside` includes
|
||||
// stock aliases from other graphs that also preserve the stock graph. Retained
|
||||
// graph rows win clashes, even against higher-priority alias contributors; other
|
||||
// fresh keys use edit order. `outside` includes
|
||||
// rows from incompatible graphs as well as out-of-range indices.
|
||||
// Selection helpers for contributors to the SAME bundle. A caller selecting a
|
||||
// different contributor must also retain that contributor's compiled resources.
|
||||
[[nodiscard]] bool preserves_stock_programs(const Table& candidate, const Table& stock);
|
||||
[[nodiscard]] bool same_program_graph(const Table& candidate, const Table& other);
|
||||
[[nodiscard]] bool only_stock_aliases(const Table& candidate, const Table& stock);
|
||||
[[nodiscard]] MergedTable merge_program_lookup(const Table& base, std::span<const Table> edits, const Table& stock = {});
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "Engine/Resource/cas_codec.h"
|
||||
#include "Engine/Resource/ebx_document.h"
|
||||
#include "Engine/Resource/toc.h"
|
||||
#include "Engine/Resource/shader_lookup.h"
|
||||
#include "Engine/Vfs/game_archives.h"
|
||||
#include "Engine/Vfs/mod_catalog.h"
|
||||
#include "Engine/Vfs/mod_merge_internal.h"
|
||||
@@ -865,9 +866,137 @@ void two_merges_write_the_same_bytes() {
|
||||
expect(read(patch / "Win32" / "pkg" / "cas_01.cas") == archive, "same bytes: the patch's own archive differs");
|
||||
expect(read(patch / "Win32" / fs::path(second_map_toc)) == toc, "same bytes: the second map's TOC differs");
|
||||
}
|
||||
|
||||
void shader_selection_checks_the_winner(bool delivered, bool changed_heap, bool alias_first, bool overwritten = false,
|
||||
bool encoder_available = true) {
|
||||
Fixture fixture("shader-selection-" + std::to_string(delivered) + std::to_string(changed_heap) +
|
||||
std::to_string(alias_first) + std::to_string(overwritten));
|
||||
const auto table = [](bool extended) {
|
||||
Resource resource{"test/program_lookup", Bytes(extended ? 544 : 208)};
|
||||
const std::uint32_t count = extended ? 2 : 1;
|
||||
const std::uint64_t start = extended ? 208 : 16;
|
||||
std::memcpy(resource.payload.data(), &count, 4);
|
||||
std::memcpy(resource.payload.data() + 4, &start, 8);
|
||||
resource.payload[192] = std::byte{7};
|
||||
put32(resource.payload, 4); // the header pointer's relocation
|
||||
return resource;
|
||||
};
|
||||
const auto rows = [](Resource resource, const std::map<std::uint64_t, std::uint32_t>& values) {
|
||||
put32(resource.payload, static_cast<std::uint32_t>(values.size()));
|
||||
for (const auto& [key, index] : values) {
|
||||
for (int shift = 0; shift < 64; shift += 8) resource.payload.push_back(static_cast<std::byte>(key >> shift));
|
||||
put32(resource.payload, 1); put32(resource.payload, index);
|
||||
}
|
||||
return resource;
|
||||
};
|
||||
auto stock = rows(table(false), {{1, 0}});
|
||||
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) {
|
||||
fs::create_directories(root / "Win32" / "pkg");
|
||||
write(root / "layout.toc", layout_toc());
|
||||
fb::BinaryBundle manifest;
|
||||
fb::BundleAsset asset;
|
||||
asset.kind = fb::AssetKind::resource; asset.name = programs.name; asset.resourceId = 42;
|
||||
asset.resourceType = fb::shader::programLookupType;
|
||||
asset.sha1 = mods::detail::sha1_of(programs.payload); asset.originalSize = programs.payload.size();
|
||||
const auto size = programs.payload.size() > 544 ? 544U : 208U;
|
||||
put32(asset.resourceMeta, size); put32(asset.resourceMeta, 4);
|
||||
put32(asset.resourceMeta, static_cast<std::uint32_t>(programs.payload.size() - size - 4)); put32(asset.resourceMeta, 0);
|
||||
manifest.resources.push_back(asset);
|
||||
const Bytes bytecode{static_cast<std::byte>(code_version)};
|
||||
if (code) {
|
||||
asset.name = "shaders/bytecode/test"; asset.resourceId = 43; 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);
|
||||
}
|
||||
auto archive = fb::write_binary_bundle(manifest);
|
||||
std::vector<fb::BundleFileInfo> files{{{patch, package, 1}, 0, static_cast<std::uint32_t>(archive.size())}};
|
||||
const auto append_payload = [&](const Bytes& payload) {
|
||||
const auto bytes = encoded(payload);
|
||||
files.push_back({{patch, package, 1}, static_cast<std::uint32_t>(archive.size()), static_cast<std::uint32_t>(bytes.size())});
|
||||
archive.insert(archive.end(), bytes.begin(), bytes.end());
|
||||
};
|
||||
append_payload(programs.payload);
|
||||
if (code) append_payload(bytecode);
|
||||
write(root / "Win32" / "pkg" / "cas_01.cas", archive);
|
||||
const std::vector<fb::TocBundle> bundles{{bundle_name, fb::write_bundle_region(files), 1}};
|
||||
write(root / "Win32" / fs::path(shared_toc), fb::write_patch_toc(bundles));
|
||||
};
|
||||
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<const char*>{"aliases", "custom"} : std::vector<const char*>{"custom", "aliases"}) {
|
||||
mods::Mod mod; mod.name = name; mod.directory = fixture.catalog.root / name; mod.provides_layout = true;
|
||||
fixture.catalog.mods.push_back(std::move(mod));
|
||||
}
|
||||
const auto report = mods::merge_mods(fixture.catalog);
|
||||
expect(report.issue.empty() && report.built, "shader graph selection builds\n" + describe(report));
|
||||
Patch patch(fixture);
|
||||
const auto toc = fb::read_toc(read(patch.root / "Win32" / fs::path(shared_toc)));
|
||||
const auto listing = mods::detail::list_bundle(patch.store, patch.root, patch.base, toc.bundles.front(), patch.game);
|
||||
expect(listing && !listing->manifest.resources.empty(), "shader result has its native resource manifest");
|
||||
if (!listing || listing->manifest.resources.empty()) return;
|
||||
const auto program = std::find_if(listing->manifest.resources.begin(), listing->manifest.resources.end(),
|
||||
[](const auto& resource) { return resource.resourceType == fb::shader::programLookupType; });
|
||||
expect(program != listing->manifest.resources.end(), "program lookup resource survives");
|
||||
if (program == listing->manifest.resources.end()) return;
|
||||
const auto program_index = listing->manifest.ebx.size() + static_cast<std::size_t>(program - listing->manifest.resources.begin());
|
||||
const auto payload = mods::detail::read_asset(patch.store, patch.root, patch.base, *listing, program_index, patch.game);
|
||||
if (payload.size() < 12) { expect(false, "program lookup payload is complete"); return; }
|
||||
if (!encoder_available) {
|
||||
// This case starts before any codec has been loaded. A failed rebuild
|
||||
// must retain the selected alias payload AND its matching metadata.
|
||||
expect(payload == aliases.payload, "failed encoding keeps the selected payload");
|
||||
std::uint32_t kept_size{}; std::memcpy(&kept_size, program->resourceMeta.data(), 4);
|
||||
expect(kept_size == 208, "failed encoding does not publish replacement graph metadata");
|
||||
expect(std::any_of(report.notes.begin(), report.notes.end(), [](const auto& note) {
|
||||
return note.find("edits could not be combined") != std::string::npos;
|
||||
}), "missing encoder exercises the failed-rebuild path");
|
||||
return;
|
||||
}
|
||||
std::uint32_t count{}; std::memcpy(&count, payload.data(), 4);
|
||||
const bool private_graph = delivered && !changed_heap && !overwritten;
|
||||
expect(count == (private_graph ? 2U : 1U), "winning and promoted custom graphs need both stock preservation and retained bundle-owned bytecode");
|
||||
const auto& meta = program->resourceMeta;
|
||||
std::uint32_t size{}, fixups{}, number{};
|
||||
std::memcpy(&size, meta.data(), 4); std::memcpy(&fixups, meta.data() + 4, 4);
|
||||
std::size_t at = static_cast<std::size_t>(size) + fixups;
|
||||
if (at + 4 > payload.size()) { expect(false, "program lookup section is bounded"); return; }
|
||||
std::memcpy(&number, payload.data() + at, 4); at += 4;
|
||||
std::set<std::uint64_t> keys;
|
||||
for (std::uint32_t row = 0; row < number; ++row) {
|
||||
if (at + 12 > payload.size()) { expect(false, "program row header is bounded"); return; }
|
||||
std::uint64_t key{}; std::uint32_t values{};
|
||||
std::memcpy(&key, payload.data() + at, 8); std::memcpy(&values, payload.data() + at + 8, 4); at += 12;
|
||||
keys.insert(key);
|
||||
for (std::uint32_t value = 0; value < values; ++value) {
|
||||
if (at + 4 > payload.size()) { expect(false, "program row indices are bounded"); return; }
|
||||
std::uint32_t index{}; std::memcpy(&index, payload.data() + at, 4); at += 4;
|
||||
expect(index < count, "native output rows stay in the retained graph's range");
|
||||
}
|
||||
}
|
||||
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");
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main() try {
|
||||
int main(int argc, char** argv) try {
|
||||
// Rebuilt native resources use the game's Oodle encoder. The normal suite
|
||||
// needs no game files; supplying a game root enables these archive checks.
|
||||
if (argc > 1 && *argv[1]) {
|
||||
(void)fb::encode_cas(Bytes{std::byte{1}}, {fs::path(argv[1])});
|
||||
shader_selection_checks_the_winner(true, false, false);
|
||||
shader_selection_checks_the_winner(true, false, true);
|
||||
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);
|
||||
} 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";
|
||||
}
|
||||
replaced_while_the_game_runs();
|
||||
const auto baseline = map_only();
|
||||
expect(baseline > 0, "map only: the map's copy exists (" + std::to_string(baseline) + ")");
|
||||
|
||||
@@ -1113,6 +1113,7 @@ fb::TocDocument combine(const fs::path& baseToc, const fs::path& baseRoot,
|
||||
for (const auto* edit : edits) editBytes.push_back(decode(*edit));
|
||||
|
||||
std::vector<std::byte> rebuilt;
|
||||
auto rebuiltMeta = entry.asset.resourceMeta;
|
||||
std::string what;
|
||||
if (refTable) {
|
||||
std::vector<fb::bundle_ref::Table> tables;
|
||||
@@ -1121,7 +1122,7 @@ fb::TocDocument combine(const fs::path& baseToc, const fs::path& baseRoot,
|
||||
const auto table = fb::bundle_ref::merge({baseBytes, baseCopy->resourceMeta}, tables);
|
||||
if (!table.added) continue;
|
||||
rebuilt = table.resource;
|
||||
entry.asset.resourceMeta = table.resourceMeta;
|
||||
rebuiltMeta = table.resourceMeta;
|
||||
what = std::to_string(table.added) + " preset(s)";
|
||||
if (table.conflicts)
|
||||
what += ", " + std::to_string(table.conflicts) + " disagreed";
|
||||
@@ -1153,15 +1154,13 @@ fb::TocDocument combine(const fs::path& baseToc, const fs::path& baseRoot,
|
||||
const auto* selected = retained;
|
||||
auto retainedBytes = decode(*retained);
|
||||
const fb::shader::Table stockTable{baseBytes, baseCopy->resourceMeta};
|
||||
if (entry.asset.resourceType == fb::shader::programLookupType &&
|
||||
fb::shader::only_stock_aliases({retainedBytes, retained->resourceMeta}, stockTable)) {
|
||||
// A stock-alias edit owns no new programs. It must not
|
||||
// erase a graph another contributor already delivers
|
||||
// to this same bundle. Highest custom contributor wins.
|
||||
for (std::size_t i = 0; i < edits.size(); ++i) {
|
||||
const fb::shader::Table candidate{editBytes[i], edits[i]->resourceMeta};
|
||||
if (fb::shader::only_stock_aliases(candidate, stockTable) ||
|
||||
!fb::shader::preserves_stock_programs(candidate, stockTable)) continue;
|
||||
if (entry.asset.resourceType == fb::shader::programLookupType) {
|
||||
// Apply identical ownership checks to the winning and
|
||||
// lower-priority custom graphs. Unproved custom heaps
|
||||
// fall back to stock, keeping safe stock aliases usable.
|
||||
const auto usable = [&](const fb::shader::Table& candidate, const Contribution* source) {
|
||||
if (fb::shader::same_program_graph(candidate, stockTable)) return true;
|
||||
if (!fb::shader::preserves_stock_programs(candidate, stockTable)) return false;
|
||||
bool dependenciesRetained = true;
|
||||
bool bytecodeDelivered = false;
|
||||
for (const auto& resource : state.assets) {
|
||||
@@ -1172,20 +1171,28 @@ fb::TocDocument combine(const fs::path& baseToc, const fs::path& baseRoot,
|
||||
const auto key = asset_key(resource.asset);
|
||||
const auto history = state.history.find(key);
|
||||
if (history == state.history.end()) continue;
|
||||
const auto shipped = state.baseSha.find(key);
|
||||
for (const auto& copy : history->second) {
|
||||
if (copy.root != edits[i]->root || copy.base ||
|
||||
(shipped != state.baseSha.end() && copy.sha1 == shipped->second)) continue;
|
||||
for (const auto& copy : history->second) if (!copy.base && copy.root == source->root) {
|
||||
if (copy.resourceType == fb::shader::bytecodeType) bytecodeDelivered = true;
|
||||
if (copy.sha1 != resource.asset.sha1 ||
|
||||
copy.resourceMeta != resource.asset.resourceMeta ||
|
||||
copy.resourceId != resource.asset.resourceId ||
|
||||
copy.resourceType != resource.asset.resourceType) dependenciesRetained = false;
|
||||
if (copy.sha1 != resource.asset.sha1 || copy.resourceMeta != resource.asset.resourceMeta ||
|
||||
copy.resourceId != resource.asset.resourceId || copy.resourceType != resource.asset.resourceType)
|
||||
dependenciesRetained = false;
|
||||
}
|
||||
}
|
||||
if (!dependenciesRetained || !bytecodeDelivered) continue;
|
||||
retained = edits[i];
|
||||
retainedBytes = editBytes[i];
|
||||
return dependenciesRetained && bytecodeDelivered;
|
||||
};
|
||||
if (!usable({retainedBytes, retained->resourceMeta}, retained)) {
|
||||
report.notes.push_back(entry.asset.name + ": custom graph ownership or stock preservation could not be proved; retaining stock programs");
|
||||
retained = baseCopy;
|
||||
retainedBytes = baseBytes;
|
||||
}
|
||||
if (fb::shader::only_stock_aliases({retainedBytes, retained->resourceMeta}, stockTable)) {
|
||||
for (std::size_t i = 0; i < edits.size(); ++i) {
|
||||
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];
|
||||
retainedBytes = editBytes[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const fb::shader::Table baseTable =
|
||||
@@ -1199,9 +1206,13 @@ fb::TocDocument combine(const fs::path& baseToc, const fs::path& baseRoot,
|
||||
report.notes.push_back(entry.asset.name + ": " + std::to_string(table.outside) +
|
||||
" material row(s) left out: their graph or indices do not match the retained "
|
||||
"bundle shader database");
|
||||
if (!table.added && retained == selected) continue;
|
||||
if (!table.added && !table.removed && retained == selected) {
|
||||
if (table.conflicts) report.notes.push_back(entry.asset.name + ": " + std::to_string(table.conflicts) +
|
||||
" material row(s) disagreed; retained graph rows win");
|
||||
continue;
|
||||
}
|
||||
rebuilt = table.resource;
|
||||
entry.asset.resourceMeta = table.resourceMeta;
|
||||
rebuiltMeta = table.resourceMeta;
|
||||
what = std::to_string(table.added) + " material row(s)";
|
||||
if (table.conflicts)
|
||||
what += ", " + std::to_string(table.conflicts) + " disagreed";
|
||||
@@ -1317,6 +1328,7 @@ fb::TocDocument combine(const fs::path& baseToc, const fs::path& baseRoot,
|
||||
const auto placement = store.write(state.manifestChunk, manifestArchive,
|
||||
fb::encode_cas(rebuilt, {gameRoot}));
|
||||
entry.file = placement;
|
||||
entry.asset.resourceMeta = std::move(rebuiltMeta);
|
||||
entry.asset.sha1 = sha1_of(rebuilt);
|
||||
entry.asset.originalSize = rebuilt.size();
|
||||
used.emplace(placement.location.installChunk, placement.location.archive);
|
||||
|
||||
+1
-1
@@ -127,7 +127,7 @@ if(DINGOSDK_BUILD_LAUNCHER_TESTS AND WIN32)
|
||||
add_executable(dingosdk_mod_merge_added_assets_tests Engine/Vfs/Test/mod_merge_added_assets_tests.cpp)
|
||||
target_link_libraries(dingosdk_mod_merge_added_assets_tests PRIVATE dingosdk_mods dingosdk_game_archives)
|
||||
target_include_directories(dingosdk_mod_merge_added_assets_tests PRIVATE "${PROJECT_SOURCE_DIR}")
|
||||
add_test(NAME mod_merge_added_assets COMMAND dingosdk_mod_merge_added_assets_tests)
|
||||
add_test(NAME mod_merge_added_assets COMMAND dingosdk_mod_merge_added_assets_tests "${DINGOSDK_TEST_GAME_ROOT}")
|
||||
add_executable(dingosdk_item_list_merge_tests Engine/Vfs/Test/item_list_merge_tests.cpp)
|
||||
target_link_libraries(dingosdk_item_list_merge_tests PRIVATE dingosdk_mods)
|
||||
target_include_directories(dingosdk_item_list_merge_tests PRIVATE "${PROJECT_SOURCE_DIR}")
|
||||
|
||||
Reference in new issue
Block a user