mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-18 23:45:22 +02:00
Shader crash fix
This commit is contained in:
1 parent
48a9cb8f5b
commit
a61f82c46a
3 files changed
+185
-31
No files matched your search
@@ -1,6 +1,9 @@
|
||||
#include "shader_lookup.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
@@ -77,32 +80,176 @@ TextureRows read_texture_rows(const Table& table) {
|
||||
|
||||
} // namespace
|
||||
|
||||
MergedTable merge_program_lookup(const Table& base, const std::span<const Table> edits) {
|
||||
const auto start = lookup_offset(base);
|
||||
auto rows = read_program_rows(base);
|
||||
const auto baseRows = rows;
|
||||
namespace {
|
||||
constexpr std::size_t programStride = 168;
|
||||
struct ProgramBlob {
|
||||
Table source;
|
||||
std::span<const std::byte> payload;
|
||||
std::uint64_t records{};
|
||||
std::uint32_t count{};
|
||||
std::vector<std::uint32_t> relocations;
|
||||
};
|
||||
ProgramBlob program_blob(const Table& source) {
|
||||
const auto end = lookup_offset(source);
|
||||
const auto size = read<std::uint32_t>(source.resourceMeta, 0);
|
||||
if ((end - size) % 4) throw std::runtime_error("Program relocations are not aligned");
|
||||
ProgramBlob blob{source, source.resource.first(size)};
|
||||
blob.count = read<std::uint32_t>(blob.payload, 0);
|
||||
blob.records = read<std::uint64_t>(blob.payload, 4);
|
||||
if (blob.records > size || blob.count > (size - blob.records) / programStride)
|
||||
throw std::runtime_error("Program records exceed their payload");
|
||||
for (auto at = std::size_t{size}; at < end; at += 4) {
|
||||
const auto pointer = read<std::uint32_t>(source.resource, at);
|
||||
if (read<std::uint64_t>(blob.payload, pointer) > size)
|
||||
throw std::runtime_error("Program relocation points outside its payload");
|
||||
blob.relocations.push_back(pointer);
|
||||
}
|
||||
return blob;
|
||||
}
|
||||
struct ProgramArray { std::size_t pointer, stride; };
|
||||
constexpr std::array programArrays{
|
||||
ProgramArray{0x10, 16}, ProgramArray{0x1c, 8}, ProgramArray{0x28, 8}, ProgramArray{0x34, 16},
|
||||
ProgramArray{0x40, 6}, ProgramArray{0x4c, 8}, ProgramArray{0x58, 2},
|
||||
ProgramArray{0x64, 8}, ProgramArray{0x70, 8}, ProgramArray{0x80, 16}, ProgramArray{0x8c, 32}
|
||||
};
|
||||
std::size_t record_at(const ProgramBlob& blob, std::uint32_t index) {
|
||||
if (index >= blob.count) throw std::runtime_error("Material references an absent shader program");
|
||||
return static_cast<std::size_t>(blob.records) + index * programStride;
|
||||
}
|
||||
std::span<const std::byte> program_array(const ProgramBlob& blob, std::size_t record, ProgramArray array) {
|
||||
const auto count = read<std::uint32_t>(blob.payload, record + array.pointer - 4);
|
||||
const auto pointer = read<std::uint64_t>(blob.payload, record + array.pointer);
|
||||
if (pointer > blob.payload.size() || count > (blob.payload.size() - pointer) / array.stride)
|
||||
throw std::runtime_error("Shader program array exceeds its payload");
|
||||
return blob.payload.subspan(static_cast<std::size_t>(pointer), count * array.stride);
|
||||
}
|
||||
bool same_program(const ProgramBlob& a, std::uint32_t ai, const ProgramBlob& b, std::uint32_t bi) {
|
||||
const auto aa = record_at(a, ai), ba = record_at(b, bi);
|
||||
for (std::size_t at = 0; at < programStride; ++at) {
|
||||
if (std::ranges::any_of(programArrays, [&](auto array) {
|
||||
return at >= array.pointer && at < array.pointer + 8; })) continue;
|
||||
if (a.payload[aa + at] != b.payload[ba + at]) return false;
|
||||
}
|
||||
for (const auto array : programArrays)
|
||||
if (!std::ranges::equal(program_array(a, aa, array), program_array(b, ba, array))) return false;
|
||||
return true;
|
||||
}
|
||||
std::uint32_t native_offset(std::size_t offset) {
|
||||
if (offset > std::numeric_limits<std::uint32_t>::max())
|
||||
throw std::runtime_error("Merged program database exceeds the native address range");
|
||||
return static_cast<std::uint32_t>(offset);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
MergedTable merge_program_lookup(const Table& base, const std::span<const Table> edits) {
|
||||
std::vector<ProgramBlob> blobs{program_blob(base)};
|
||||
for (const auto& edit : edits) blobs.push_back(program_blob(edit));
|
||||
struct Reference { std::size_t blob; std::uint32_t program; };
|
||||
std::map<std::uint64_t, std::vector<Reference>> references;
|
||||
auto rows = read_program_rows(base);
|
||||
for (const auto& [key, values] : rows) for (const auto index : values) {
|
||||
record_at(blobs.front(), index);
|
||||
references[key].push_back({0, index});
|
||||
}
|
||||
const auto baseRows = rows;
|
||||
MergedTable merged;
|
||||
for (const auto& edit : edits) {
|
||||
for (const auto& [key, values] : read_program_rows(edit)) {
|
||||
for (std::size_t b = 1; b < blobs.size(); ++b) {
|
||||
for (const auto& [key, values] : read_program_rows(blobs[b].source)) {
|
||||
for (const auto index : values) record_at(blobs[b], index);
|
||||
if (baseRows.contains(key)) continue;
|
||||
const auto [row, inserted] = rows.try_emplace(key, values);
|
||||
if (inserted) ++merged.added;
|
||||
else if (row->second != values) ++merged.conflicts;
|
||||
if (const auto previous = references.find(key); previous != references.end()) {
|
||||
bool same = previous->second.size() == values.size();
|
||||
for (std::size_t i = 0; same && i < values.size(); ++i) {
|
||||
const auto ref = previous->second[i];
|
||||
same = same_program(blobs[ref.blob], ref.program, blobs[b], values[i]);
|
||||
}
|
||||
if (!same) ++merged.conflicts;
|
||||
continue;
|
||||
}
|
||||
for (const auto index : values) references[key].push_back({b, index});
|
||||
++merged.added;
|
||||
}
|
||||
}
|
||||
|
||||
merged.resource.assign(base.resource.begin(),
|
||||
base.resource.begin() + static_cast<std::ptrdiff_t>(start));
|
||||
append(merged.resource, static_cast<std::uint32_t>(rows.size()));
|
||||
for (const auto& [key, values] : rows) {
|
||||
append(merged.resource, key);
|
||||
append(merged.resource, static_cast<std::uint32_t>(values.size()));
|
||||
for (const auto value : values) append(merged.resource, value);
|
||||
// Preserve the exact native prefix when edits only add aliases to stock programs.
|
||||
// Private programs need their own records and relocated backing data as well.
|
||||
using Source = std::pair<std::size_t, std::uint32_t>;
|
||||
std::map<Source, std::uint32_t> remapped;
|
||||
std::vector<Source> additions;
|
||||
for (const auto& [key, refs] : references) {
|
||||
auto& row = rows[key];
|
||||
row.clear();
|
||||
for (const auto ref : refs) {
|
||||
auto index = ref.program;
|
||||
if (ref.blob && (index >= blobs.front().count ||
|
||||
!same_program(blobs.front(), index, blobs[ref.blob], index))) {
|
||||
const Source source{ref.blob, index};
|
||||
auto [entry, inserted] = remapped.try_emplace(source,
|
||||
native_offset(std::size_t{blobs.front().count} + additions.size()));
|
||||
if (inserted) additions.push_back(source);
|
||||
index = entry->second;
|
||||
}
|
||||
row.push_back(index);
|
||||
}
|
||||
}
|
||||
merged.resourceMeta.assign(base.resourceMeta.begin(), base.resourceMeta.end());
|
||||
const auto size = static_cast<std::uint32_t>(merged.resource.size() - start);
|
||||
std::memcpy(merged.resourceMeta.data() + 8, &size, sizeof(size));
|
||||
if (additions.empty()) {
|
||||
merged.resource.assign(base.resource.begin(), base.resource.begin() + lookup_offset(base));
|
||||
} else {
|
||||
merged.resource.assign(blobs.front().payload.begin(), blobs.front().payload.end());
|
||||
auto relocations = blobs.front().relocations;
|
||||
std::map<std::size_t, std::size_t> offsets;
|
||||
for (const auto& [b, index] : additions) {
|
||||
if (offsets.contains(b)) continue;
|
||||
merged.resource.resize((merged.resource.size() + 15) & ~std::size_t{15});
|
||||
const auto offset = merged.resource.size();
|
||||
offsets[b] = offset;
|
||||
const auto& blob = blobs[b];
|
||||
// Copy the complete backing graph, including opaque resource bindings.
|
||||
// This keeps runtime-mutated GUID arrays separate across mods.
|
||||
merged.resource.insert(merged.resource.end(), blob.payload.begin(), blob.payload.end());
|
||||
for (const auto pointer : blob.relocations) {
|
||||
const auto destination = native_offset(offset + pointer);
|
||||
const auto target = read<std::uint64_t>(blob.payload, pointer) + offset;
|
||||
std::memcpy(merged.resource.data() + destination, &target, 8);
|
||||
relocations.push_back(destination);
|
||||
}
|
||||
}
|
||||
merged.resource.resize((merged.resource.size() + 7) & ~std::size_t{7});
|
||||
const auto table = static_cast<std::uint64_t>(merged.resource.size());
|
||||
const auto count = native_offset(std::size_t{blobs.front().count} + additions.size());
|
||||
merged.resource.resize(native_offset(static_cast<std::size_t>(table) + std::size_t{count} * programStride));
|
||||
const auto copy_record = [&](const ProgramBlob& blob, std::uint32_t index,
|
||||
std::size_t offset, std::uint32_t destination) {
|
||||
const auto source = record_at(blob, index);
|
||||
const auto target = static_cast<std::size_t>(table) + destination * programStride;
|
||||
std::memcpy(merged.resource.data() + target, merged.resource.data() + offset + source, programStride);
|
||||
for (const auto pointer : blob.relocations)
|
||||
if (pointer >= source && pointer < source + programStride)
|
||||
relocations.push_back(native_offset(target + pointer - source));
|
||||
};
|
||||
for (std::uint32_t i = 0; i < blobs.front().count; ++i) copy_record(blobs.front(), i, 0, i);
|
||||
for (const auto& source : additions)
|
||||
copy_record(blobs[source.first], source.second, offsets.at(source.first), remapped.at(source));
|
||||
std::memcpy(merged.resource.data(), &count, 4);
|
||||
std::memcpy(merged.resource.data() + 4, &table, 8);
|
||||
relocations.push_back(4);
|
||||
std::ranges::sort(relocations);
|
||||
relocations.erase(std::ranges::unique(relocations).begin(), relocations.end());
|
||||
const auto size = native_offset(merged.resource.size());
|
||||
for (const auto pointer : relocations) append(merged.resource, pointer);
|
||||
const auto relocationSize = native_offset(relocations.size() * 4);
|
||||
std::memcpy(merged.resourceMeta.data(), &size, 4);
|
||||
std::memcpy(merged.resourceMeta.data() + 4, &relocationSize, 4);
|
||||
}
|
||||
const auto lookup = merged.resource.size();
|
||||
append(merged.resource, native_offset(rows.size()));
|
||||
for (const auto& [key, values] : rows) {
|
||||
append(merged.resource, key);
|
||||
append(merged.resource, native_offset(values.size()));
|
||||
for (const auto value : values) append(merged.resource, value);
|
||||
}
|
||||
const auto size = native_offset(merged.resource.size() - lookup);
|
||||
std::memcpy(merged.resourceMeta.data() + 8, &size, 4);
|
||||
return merged;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
// The two shader lookup tables a level superbundle carries. A map mod adds a
|
||||
// row per material it introduces, aliasing a shader program and texture set the
|
||||
// base already ships, so two mods' tables differ from the base only by the rows
|
||||
// each added and can be combined exactly.
|
||||
// Shader lookup tables carry material rows and compiled program records.
|
||||
// Mods can alias stock programs or add private programs with their own
|
||||
// permutations and backing arrays; both must survive a merge.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -27,9 +26,9 @@ struct MergedTable {
|
||||
std::size_t conflicts{}; // rows two edits disagreed on; the first won
|
||||
};
|
||||
|
||||
// `resourceMeta` is 16 bytes: program size, relocation size, lookup size. The
|
||||
// program blob and its relocations are identical across copies because a mod
|
||||
// only ever points new keys at programs that are already there.
|
||||
// `resourceMeta` is 16 bytes: program size, relocation size, lookup size.
|
||||
// Private program records and their relocated backing data are copied from
|
||||
// each edit, and material indices are remapped into the combined table.
|
||||
[[nodiscard]] MergedTable merge_program_lookup(const Table& base, std::span<const Table> edits);
|
||||
|
||||
[[nodiscard]] MergedTable merge_texture_lookup(const Table& base, std::span<const Table> edits);
|
||||
|
||||
@@ -788,7 +788,10 @@ fb::TocDocument combine(const fs::path& baseToc, const fs::path& baseRoot,
|
||||
// holds one asset per name, so the lower-priority mod's is gone,
|
||||
// and whatever of that mod's still names it by guid finds nothing
|
||||
// (an item list entry is dropped for that further down).
|
||||
if (entry.asset.kind != fb::AssetKind::chunk && !rewritten && previous.mod &&
|
||||
const bool shaderTable = entry.asset.kind == fb::AssetKind::resource &&
|
||||
(entry.asset.resourceType == fb::shader::programLookupType ||
|
||||
entry.asset.resourceType == fb::shader::textureLookupType);
|
||||
if (entry.asset.kind != fb::AssetKind::chunk && !shaderTable && !rewritten && previous.mod &&
|
||||
previous.mod != owner && previous.asset.sha1 != entry.asset.sha1 &&
|
||||
!state.baseSha.contains(at->first)) {
|
||||
auto& [count, example] = replaced[*previous.mod];
|
||||
@@ -846,6 +849,11 @@ fb::TocDocument combine(const fs::path& baseToc, const fs::path& baseRoot,
|
||||
const Contribution* baseCopy{};
|
||||
for (const auto& contribution : found->second)
|
||||
if (contribution.base) { baseCopy = &contribution; break; }
|
||||
// Custom cosmetic bundles also carry the global shader tables,
|
||||
// although the game's copy of that bundle does not. Use the
|
||||
// highest-priority contributor as the starting table there.
|
||||
const bool addedShaderTable = !baseCopy && shaderTable;
|
||||
if (addedShaderTable) baseCopy = &found->second.back();
|
||||
if (!baseCopy) continue;
|
||||
// Both mods rebuild the whole bundle, so only the assets whose
|
||||
// content actually moved away from the base are contested.
|
||||
@@ -856,7 +864,8 @@ fb::TocDocument combine(const fs::path& baseToc, const fs::path& baseRoot,
|
||||
// An item list is looked at even when one mod alone changed it:
|
||||
// what its entries name has to be in the bundle either way.
|
||||
const auto named = entry.asset.kind == fb::AssetKind::ebx && item_list_name(entry.asset.name);
|
||||
if (edits.size() < 2 && !(named && edits.size() == 1)) continue;
|
||||
if (edits.size() < 2 && !(named && edits.size() == 1) &&
|
||||
!(addedShaderTable && !edits.empty())) continue;
|
||||
try {
|
||||
const auto baseBytes = decode(*baseCopy);
|
||||
std::vector<std::vector<std::byte>> editBytes;
|
||||
@@ -889,9 +898,8 @@ fb::TocDocument combine(const fs::path& baseToc, const fs::path& baseRoot,
|
||||
std::to_string(clash.first) + " preset(s) share a short name with another mod's, e.g. " +
|
||||
clash.second + "; they are registered by their full path only");
|
||||
} else if (shaderTable) {
|
||||
// Each mod aliases new material keys onto shader programs
|
||||
// and texture sets the base already ships, so the union of
|
||||
// the rows they added is exactly right.
|
||||
// Keep each private program and its backing arrays, then
|
||||
// remap material rows into the combined program table.
|
||||
std::vector<fb::shader::Table> tables;
|
||||
for (std::size_t index = 0; index < editBytes.size(); ++index)
|
||||
tables.push_back({editBytes[index], edits[index]->resourceMeta});
|
||||
|
||||
Reference in new issue
Block a user