gpu: HayatoG's newer core (shader cache index fix, recoverable shader failures, bounded compile wait, texture-cache switches); draw distance for 1.07

Merged three-way from iss_pc 5b55292..907ab47 (GPL-2.0). The draw distance hooks (src/view_hook.h,
gpu/shim/gow3_view.cpp, signature-checked on the 01.07 eboot) take GOW3_LOD_SCALE,
GOW3_REGION_FADE and the other view settings; the profile uses HayatoG's recommended 0.6 / 0.8:
fewer draws in the open city, this port's bottleneck. The dynamic quality threshold patch is
off by default: the game's own quality steps lower the draw count when frames get heavy. Eager
read-ahead is off (it made smoke flicker there); BP06 off on AMD.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XzE465ySHDfaMBVSJyYtNw
This commit is contained in:
Claude committed 2026-10-16 14:27:10 -03:00
1 parent 4917186871
commit dae6830297
28 files changed
+1386 -71

No files matched your search

+14 -2
View File
@@ -53,13 +53,13 @@ inline constexpr std::array<Entry, static_cast<std::size_t>(Id::Count)> kEntries
{"BP03", "GOW3_BP03", true, true, "keep EXEC after an empty divergent branch", "shadPS4 c34ca21f"},
{"BP04", "GOW3_BP04", true, true, "V_RCP/RSQ/LOG_CLAMP and V_RSQ_LEGACY really clamp to +-FLT_MAX", "shadPS4 1c2b8f97 #5346"},
{"BP05", "GOW3_BP05", true, true, "saturate maps NaN to 0 like DX10_CLAMP (NClamp)", "shadPS4 #5257"},
{"BP06", "GOW3_BP06", false, true, "VK_KHR_shader_maximal_reconvergence in compute", "SotC 8d84cf7"},
{"BP06", "GOW3_BP06", true, true, "VK_KHR_shader_maximal_reconvergence in compute", "SotC 8d84cf7"},
{"BP07", "GOW3_BP07", false, false, "async compile never skips full-screen/indirect draws", "bbport 10baf9d"},
{"BP08", "GOW3_BP08", false, false, "persist the driver VkPipelineCache blob", "Mrsuss b972798, 0xCydral bdb9769"},
{"BP09", "GOW3_BP09", false, true, "every 1D image as 2D with height 1", "CTR d26bcb2"},
{"BP10", "GOW3_BP10", true, false, "barriers for vertex/index/indirect buffers", "shadPS4 #5262"},
{"BP11", "GOW3_BP11", true, true, "firstInstance counted once in indirect draws", "shadPS4 #5348"},
{"BP12", "GOW3_BP12", true, true, "EarlyFragmentTests when DEPTH_BEFORE_SHADER is set", "shadPS4 7d28d279 #5264"},
{"BP12", "GOW3_BP12", false, true, "EarlyFragmentTests when DEPTH_BEFORE_SHADER is set", "shadPS4 7d28d279 #5264"},
}};
namespace Detail {
@@ -130,6 +130,16 @@ inline std::array<std::atomic<std::int8_t>, kEntries.size()>& States() {
}
} // namespace Detail
/// GOW3_OCCLUSION_MODE (ISS_ alias): how the ZPASS_DONE occlusion counter is answered. 0 = the old fake,
/// ever-growing counter, 1 = both values equal, 2 = end far above begin, 3 = zero counts (default).
inline int OcclusionMode() {
static const int mode = [] {
const char* v = std::getenv("GOW3_OCCLUSION_MODE");
return v && *v ? std::atoi(v) : 3;
}();
return mode;
}
/// State of one backport.
inline bool On(Id id) {
return Detail::States()[static_cast<std::size_t>(id)].load(std::memory_order_relaxed) != 0;
@@ -191,6 +201,8 @@ inline std::uint32_t ShaderSalt() {
/// One line per backport, once at start-up (the run log keeps stdout).
inline void LogStates() {
std::printf("Backports: occlusion mode %d (GOW3_OCCLUSION_MODE; 0 = old fake counter, 3 = default)\n",
OcclusionMode());
for (std::size_t i = 0; i < kEntries.size(); ++i) {
const Entry& e = kEntries[i];
std::printf("Backports: %s %s=%d (%s; %s)\n", e.id, e.env,
@@ -16,6 +16,7 @@
#include "core/emulator_settings.h"
#include "core/signals.h"
#include "shader_recompiler/info.h"
#include "shader_recompiler/recompiler.h"
#include "shader_recompiler/ir/ir_emitter.h"
#include "shader_recompiler/ir/opcodes.h"
#include "shader_recompiler/ir/passes/srt.h"
@@ -835,8 +836,17 @@ void FlattenExtendedUserdataPass(IR::Program& program) {
inst && inst->GetOpcode() == IR::Opcode::ReadFirstLane) {
continue;
}
ASSERT_MSG(IsReadConstSource(base->Arg(0)), "ReadConst base low not from constant memory");
ASSERT_MSG(IsReadConstSource(base->Arg(1)), "ReadConst base high not from constant memory");
// gow3: a ReadConst pattern this pass does not know (new area of inFAMOUS, 2026-10-15) used
// to abort the whole game. The shader is reported and refused instead: the pipeline cache
// marks it failed and the draws that use it are skipped.
if (!IsReadConstSource(base->Arg(0)) || !IsReadConstSource(base->Arg(1))) {
const auto message = fmt::format(
"shader {:#018x} ({}): ReadConst base {} not from constant memory",
program.info.pgm_hash, program.info.hw_stage,
IsReadConstSource(base->Arg(0)) ? "high" : "low");
LOG_ERROR(Render_Recompiler, "gow3: FlattenExtendedUserdataPass: {}", message);
throw Shader::UnsupportedShaderError(message);
}
IR::Inst* ptr_lo = base->Arg(0).Inst();
ptr_lo = pass_info.DeduplicateInstruction(ptr_lo);
@@ -3,6 +3,7 @@
#pragma once
#include <stdexcept>
#include "common/object_pool.h"
#include "shader_recompiler/ir/basic_block.h"
#include "shader_recompiler/ir/program.h"
@@ -12,6 +13,13 @@ namespace Shader {
struct Profile;
struct RuntimeInfo;
/// gow3: thrown by a recompiler pass for a shader it cannot translate. The pipeline cache marks
/// the shader (or its permutation) failed and skips the draws that use it, where an ASSERT would
/// end the whole game.
struct UnsupportedShaderError : std::runtime_error {
using std::runtime_error::runtime_error;
};
struct Pools {
static constexpr u32 InstPoolSize = 8192;
static constexpr u32 BlockPoolSize = 32;
+29 -2
View File
@@ -1147,8 +1147,35 @@ Liverpool::Task Liverpool::ProcessGraphics(std::span<const u32> dcb, std::span<c
static constexpr u64 OcclusionCounterValidMask = 0x8000000000000000ULL;
static constexpr u64 OcclusionCounterStep = 0x2FFFFFFULL;
u64* results = event->Address<u64*>();
for (s32 i = 0; i < num_counter_pairs; ++i, results += 2) {
*results = pixel_counter | OcclusionCounterValidMask;
// gow3: occlusion probe (GOW3_OCCLUSION_LOG=1 logs the events; GOW3_OCCLUSION_MODE
// 0 = fake counter as before, 1 = both slots equal (nothing visible), 2 = end far
// above begin (everything visible), 3 = zero counts: the default).
const int occ_mode = Common::Backports::OcclusionMode();
static const bool occ_log = [] {
const char* v = std::getenv("GOW3_OCCLUSION_LOG");
return v && *v && std::atoi(v) != 0;
}();
static std::atomic<u64> occ_events{0};
const u64 occ_n = occ_events.fetch_add(1, std::memory_order_relaxed);
if (occ_log && (occ_n < 48 || occ_n % 2000 == 0)) {
std::printf("Occlusion: event %llu addr %p pairs %d before: %016llx %016llx %016llx %016llx\n",
static_cast<unsigned long long>(occ_n), static_cast<void*>(results),
num_counter_pairs, static_cast<unsigned long long>(results[0]),
static_cast<unsigned long long>(results[1]),
static_cast<unsigned long long>(results[2]),
static_cast<unsigned long long>(results[3]));
}
if (occ_mode == 0) {
for (s32 i = 0; i < num_counter_pairs; ++i, results += 2) {
*results = pixel_counter | OcclusionCounterValidMask;
}
} else {
for (s32 i = 0; i < num_counter_pairs; ++i, results += 2) {
const u64 v = pixel_counter | OcclusionCounterValidMask;
results[0] = occ_mode == 3 ? OcclusionCounterValidMask : v;
results[1] = occ_mode == 1 ? v : occ_mode == 2 ? v + (OcclusionCounterStep << 8) :
OcclusionCounterValidMask;
}
}
pixel_counter += OcclusionCounterStep;
}
+23
View File
@@ -374,6 +374,29 @@ void DataBase::ForEachBlob(BlobType type, const std::function<void(std::vector<u
}
}
void DataBase::ForEachBlobName(BlobType type,
const std::function<void(std::string_view name)>& func) {
const auto ext = "." + GetBlobFileExtension(type);
const auto visit = [&](std::string_view name) {
if (name.ends_with(ext)) {
func(name.substr(0, name.size() - ext.size()));
}
};
if (EmulatorSettings.IsPipelineCacheArchived()) {
const auto num_files = mz_zip_reader_get_num_files(&zip_ar);
for (int index = 0; index < num_files; ++index) {
std::array<char, MZ_ZIP_MAX_ARCHIVE_FILENAME_SIZE> file_name{};
mz_zip_reader_get_filename(&zip_ar, index, file_name.data(), file_name.size());
visit(file_name.data());
}
return;
}
std::error_code error;
for (const auto& entry : std::filesystem::directory_iterator{cache_path, error}) {
visit(entry.path().filename().string());
}
}
void DataBase::Clear() {
if (!opened || EmulatorSettings.IsPipelineCacheArchived()) {
return;
+4
View File
@@ -8,6 +8,8 @@
#include "common/types.h"
#include <functional>
#include <string>
#include <string_view>
#include <thread>
#include <vector>
@@ -53,6 +55,8 @@ public:
[[nodiscard]] bool ConcurrentReads() const;
/// gow3: number of blobs ForEachBlob visits (the loading screen's total).
size_t CountBlobs(BlobType type);
/// gow3: the name (no extension) of every blob of a type, without reading it.
void ForEachBlobName(BlobType type, const std::function<void(std::string_view name)>& func);
private:
std::jthread io_worker{};
@@ -125,6 +125,11 @@ public:
future.wait();
}
}
/// True when the job has finished (or there is none) within `timeout`.
template <typename Rep, typename Period>
bool WaitFor(std::chrono::duration<Rep, Period> timeout) const {
return !future.valid() || future.wait_for(timeout) == std::future_status::ready;
}
std::optional<T> Poll() {
if (!future.valid() || future.wait_for(std::chrono::seconds{0}) != std::future_status::ready) {
return std::nullopt;
@@ -268,7 +268,9 @@ GraphicsPipeline::GraphicsPipeline(
.module = modules[stage],
.pName = "main",
});
} else if (runtime_infos[u32(Shader::SwStage::Fragment)].hw.fs.clip_distance_emulation) {
} else if (preloading ? !sdata.fragment.empty()
: runtime_infos[u32(Shader::SwStage::Fragment)].hw.fs.clip_distance_emulation) {
// gow3: a preloaded pipeline has no runtime infos; its .key carries the discard shader.
if (!preloading) {
const auto& vs = runtime_infos[static_cast<u32>(Shader::SwStage::Vertex)].hw.vs;
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <string_view>
#include <cstdio>
#include <cstdlib>
#include <boost/container/static_vector.hpp>
@@ -901,7 +902,11 @@ void Instance::CollectDeviceParameters() {
// gow3: see video_core/amdgpu/gow3_bc_tiny.h. Set before any image or shader exists.
const char* bc_tiny_env = std::getenv("GOW3_BC_TINY");
const bool bc_tiny = bc_tiny_env && *bc_tiny_env
// GOW3_BC_TINY=auto: also decode on any driver that refuses BC 1D and is not given the 2D
// promotion (NVIDIA), so the driver never sees a block-compressed 1D image (test branch).
const bool bc_tiny_auto = bc_tiny_env && std::string_view{bc_tiny_env} == "auto";
const bool bc_tiny = bc_tiny_auto ? (bc1d_refused && !block1d_as_2d)
: bc_tiny_env && *bc_tiny_env
? *bc_tiny_env == '1'
: false; // issn: off as tested (1D images are 2D here)
AmdGpu::bc_tiny_decode.store(bc_tiny, std::memory_order_relaxed);
@@ -2,6 +2,8 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <atomic>
#include <string_view>
#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <cstdio>
@@ -16,6 +18,7 @@
#include <xxhash.h>
#include "common/backports.h"
#include "gow3_threads.h"
#include "gow3_region.h"
#include "common/elf_info.h"
#include "common/hash.h"
@@ -158,6 +161,19 @@ static u32 MapOutputs(std::span<Shader::OutputMap, 3> outputs, const AmdGpu::VsO
return num_outputs;
}
// Lowered user clip planes ride the same emulation path as guest-exported distances, so the
// fragment side arms whenever the hardware vertex stage lowers them, keeping its input locations in
// sync with the shifted vertex outputs.
static bool NeedsClipDistanceEmulation(const AmdGpu::Regs& regs, const Shader::Profile& profile) {
const bool lowers_user_clip_planes =
regs.clipper_control.user_clip_plane_enable &&
!regs.stage_enable.IsStageEnabled(static_cast<u32>(HwStage::Geometry));
return ((regs.vs_output_control.clip_distance_enable &&
!regs.stage_enable.IsStageEnabled(static_cast<u32>(HwStage::Local))) ||
lowers_user_clip_planes) &&
profile.needs_clip_distance_emulation;
}
const Shader::RuntimeInfo& PipelineCache::BuildRuntimeInfo(PipelineSelection& sel, HwStage stage,
SwStage l_stage) {
auto& info = sel.runtime_infos[u32(l_stage)];
@@ -285,17 +301,7 @@ const Shader::RuntimeInfo& PipelineCache::BuildRuntimeInfo(PipelineSelection& se
for (u32 i = 0; i < Shader::MaxColorBuffers; i++) {
info.hw.fs.color_buffers[i] = sel.graphics_key.color_buffers[i];
}
// Lowered user clip planes ride the same emulation path as guest-exported distances, so
// the fragment side arms whenever the hardware vertex stage lowers them, keeping its input
// locations in sync with the shifted vertex outputs.
const bool lowers_user_clip_planes =
regs.clipper_control.user_clip_plane_enable &&
!regs.stage_enable.IsStageEnabled(static_cast<u32>(HwStage::Geometry));
info.hw.fs.clip_distance_emulation =
((regs.vs_output_control.clip_distance_enable &&
!regs.stage_enable.IsStageEnabled(static_cast<u32>(HwStage::Local))) ||
lowers_user_clip_planes) &&
profile.needs_clip_distance_emulation;
info.hw.fs.clip_distance_emulation = NeedsClipDistanceEmulation(regs, profile);
break;
}
case HwStage::Compute: {
@@ -497,7 +503,13 @@ void PipelineCache::StartDriverCacheSaver() {
auto* saver = driver_saver;
std::unique_lock lock{saver->mutex};
while (!saver->stop) {
if (saver->cv.wait_for(lock, std::chrono::seconds(180), [&] { return saver->stop; })) {
// gow3: GOW3_DRIVER_CACHE_SAVE_S (default 60, was 180): a crash loses at most this much of what
// the driver learned.
static const int save_seconds = [] {
const char* v = std::getenv("GOW3_DRIVER_CACHE_SAVE_S");
return v && *v ? std::clamp(std::atoi(v), 10, 3600) : 60;
}();
if (saver->cv.wait_for(lock, std::chrono::seconds(save_seconds), [&] { return saver->stop; })) {
break;
}
lock.unlock();
@@ -771,7 +783,12 @@ bool PipelineCache::QueueShader(Program& program, HwStage stage, SwStage l_stage
return false;
}
runtime_fault_recover = &recover;
capture_work();
try {
capture_work();
} catch (...) {
runtime_fault_recover = nullptr; // `recover` dies with this frame
throw;
}
runtime_fault_recover = nullptr;
return true;
})) {
@@ -781,11 +798,7 @@ bool PipelineCache::QueueShader(Program& program, HwStage stage, SwStage l_stage
PhaseTimer waiting{g_gow3_guest_wait_ns};
capture_job.Wait();
}
if (faulted->load(std::memory_order_acquire)) {
PhaseTimer capture{g_gow3_capture_ns};
build->ir.reset();
capture_work();
} else if (!capture_job.Poll()) {
const auto mark_failed = [&](const std::string& error) {
if (initial) {
program.compilation_failed = true;
} else {
@@ -793,7 +806,19 @@ bool PipelineCache::QueueShader(Program& program, HwStage stage, SwStage l_stage
program.failed_permutations.push_back(std::move(*specialization));
}
LOG_ERROR(Render_Vulkan, "gow3: shader {:#x}/{} capture failed: {}", params.hash,
permutation, capture_job.Error());
permutation, error);
};
if (faulted->load(std::memory_order_acquire)) {
PhaseTimer capture{g_gow3_capture_ns};
build->ir.reset();
try {
capture_work();
} catch (const std::exception& e) {
mark_failed(e.what());
return false;
}
} else if (!capture_job.Poll()) {
mark_failed(capture_job.Error());
return false;
}
const bool queued = build->job.Start(*compiler, [this, build] {
@@ -865,7 +890,7 @@ void PipelineCache::PublishShaders(bool wait) {
build->spec.info = &program->info;
RegisterShaderMeta(program->info, build->spec.fetch_shader_data, build->spec,
perm_hash, build->permutation);
program->AddPermut(build->module, std::move(build->spec));
program->InsertPermut(build->module, std::move(build->spec), build->permutation);
build->module = nullptr;
program->pending.reset();
}
@@ -1093,6 +1118,66 @@ void Bp07Tally(bool compiled_now) {
}
} // namespace
// gow3: GOW3_ASYNC_WAIT_MS=N (default 0 = off), GOW3_ASYNC_WAIT_FRAME_MS=M (default 8). A draw whose
// shader or pipeline is still compiling in the background is skipped. With N > 0 it first waits
// for the compile up to N ms, and the waits of one GPU frame add up to at most M ms: a short
// compile then costs a few milliseconds instead of a missing ground or effect, a long one is
// skipped as before, so the frame time stays bounded. The log shows the tally every 5 s.
// A new shader is the usual reason for a skipped draw (the pipeline is only queued once the
// shader is translated), so GetProgram waits too, not only GetGraphicsPipeline.
namespace {
struct AsyncWaitBudget {
std::chrono::microseconds per_draw{0}, per_frame{8000};
u64 frame = ~0ull;
std::chrono::nanoseconds used{0};
u64 waited = 0, ready = 0, timed_out = 0;
std::chrono::nanoseconds total{0};
std::chrono::steady_clock::time_point last_log = std::chrono::steady_clock::now();
AsyncWaitBudget() {
if (const char* v = std::getenv("GOW3_ASYNC_WAIT_MS"); v && *v) {
per_draw = std::chrono::milliseconds(std::clamp(std::atoi(v), 0, 100));
}
if (const char* v = std::getenv("GOW3_ASYNC_WAIT_FRAME_MS"); v && *v) {
per_frame = std::chrono::milliseconds(std::clamp(std::atoi(v), 0, 100));
}
}
bool Enabled() const { return per_draw.count() > 0 && per_frame.count() > 0; }
/// How long this draw may wait now (zero: none left in this frame).
std::chrono::nanoseconds Allowance() {
const u64 now_frame = Gow3Stats::gpu_frames.load(std::memory_order_relaxed);
if (now_frame != frame) {
frame = now_frame;
used = std::chrono::nanoseconds{0};
}
const auto left = std::chrono::duration_cast<std::chrono::nanoseconds>(per_frame) - used;
return left.count() <= 0 ? std::chrono::nanoseconds{0}
: std::min<std::chrono::nanoseconds>(left, per_draw);
}
void Tally(std::chrono::nanoseconds spent, bool finished) {
used += spent;
total += spent;
++waited;
++(finished ? ready : timed_out);
const auto now = std::chrono::steady_clock::now();
if (now - last_log < std::chrono::seconds(5)) {
return;
}
last_log = now;
// A warning, not info: the log keeps only warnings and above.
LOG_WARNING(Render_Vulkan,
"Async wait (total): {} waits for a compiling shader or pipeline, {:.1f} ms in all, "
"{} finished in time, {} gave up at the limit and the draw was skipped "
"({} ms per wait, {} ms per frame)",
waited, double(total.count()) / 1e6, ready, timed_out, per_draw.count() / 1000,
per_frame.count() / 1000);
}
};
AsyncWaitBudget async_wait; // GPU thread only (pipeline selection)
} // namespace
const GraphicsPipeline* PipelineCache::GetGraphicsPipeline(const DrawIndirectParams params,
const PreparedDraw* prepared) {
SeedWorkerCaches();
@@ -1109,6 +1194,7 @@ const GraphicsPipeline* PipelineCache::GetGraphicsPipeline(const DrawIndirectPar
if (prepared) {
if (const auto* pipeline = TryPreparedPipeline(*prepared)) {
used_prepared = prepared;
Gow3Region::NotePipeline('g', std::hash<GraphicsPipelineKey>{}(prepared->key), true);
return pipeline;
}
}
@@ -1122,6 +1208,9 @@ const GraphicsPipeline* PipelineCache::GetGraphicsPipeline(const DrawIndirectPar
}
return nullptr;
}
// gow3: region trace (GOW3_REGION_TRACE=1): the first request of each pipeline.
Gow3Region::NotePipeline('g', std::hash<GraphicsPipelineKey>{}(sel.graphics_key),
graphics_pipelines.contains(sel.graphics_key));
if (AsyncCompilation() && !sel.regs->stage_enable.hs_en && !EmulatorSettings.IsShaderCollect()) {
const auto found = graphics_pipelines.find(sel.graphics_key);
if (found != graphics_pipelines.end()) {
@@ -1174,6 +1263,24 @@ const GraphicsPipeline* PipelineCache::GetGraphicsPipeline(const DrawIndirectPar
pending_graphics.emplace(sel.graphics_key, std::move(build));
}
}
// gow3: GOW3_ASYNC_WAIT_MS: give the background compile a few milliseconds before the draw goes.
if (async_wait.Enabled()) {
if (const auto building = pending_graphics.find(sel.graphics_key);
building != pending_graphics.end()) {
if (const auto allowance = async_wait.Allowance(); allowance.count() > 0) {
const auto t0 = std::chrono::steady_clock::now();
const bool finished = building.value()->job.WaitFor(allowance);
async_wait.Tally(std::chrono::steady_clock::now() - t0, finished);
if (finished) {
PublishGraphics();
if (const auto done = graphics_pipelines.find(sel.graphics_key);
done != graphics_pipelines.end()) {
return done->second.get();
}
}
}
}
}
++g_gow3_pending_draws;
if (bp07) {
Bp07Tally(false);
@@ -1216,6 +1323,8 @@ const ComputePipeline* PipelineCache::GetComputePipeline() {
if (!RefreshComputeKey()) {
return nullptr;
}
Gow3Region::NotePipeline('c', std::hash<ComputePipelineKey>{}(compute_key),
compute_pipelines.contains(compute_key));
const auto [it, is_new] = compute_pipelines.try_emplace(compute_key);
if (is_new) {
const auto pipeline_hash = std::hash<ComputePipelineKey>{}(compute_key);
@@ -1479,6 +1588,14 @@ bool PipelineCache::RefreshGraphicsStages(PipelineSelection& sel) {
}
const auto* fs_info = sel.infos[static_cast<u32>(SwStage::Fragment)];
if (!fs_info) {
// gow3: with no pixel shader, GraphicsPipeline adds the discard fragment shader from this
// runtime info. It used to keep the previous draw's, so a pipeline got the discard shader
// (or lost it) depending on the draw before it (NVIDIA clip distance emulation).
auto& fs_runtime = sel.runtime_infos[static_cast<u32>(SwStage::Fragment)];
fs_runtime = {};
fs_runtime.hw.fs.clip_distance_emulation = NeedsClipDistanceEmulation(regs, profile);
}
key.mrt_mask = fs_info ? fs_info->mrt_mask : 0u;
key.num_color_attachments = std::bit_width(key.mrt_mask);
@@ -1673,6 +1790,10 @@ void Program::LogPermutationGrowth() const {
DescribeSpecDiff(modules[n - 2].spec, modules[n - 1].spec));
}
bool Program::PendingInitial() const {
return pending && pending->initial;
}
PipelineCache::Result PipelineCache::GetProgram(PipelineSelection& sel, HwStage hw_stage,
SwStage sw_stage,
const Shader::ShaderParams& params,
@@ -1718,6 +1839,26 @@ PipelineCache::Result PipelineCache::GetProgram(PipelineSelection& sel, HwStage
HashCombine(params.hash, perm_idx));
}
// gow3: GOW3_ASYNC_WAIT_MS: before the draw goes for lack of its shader, give the background
// translation a few milliseconds. True when the shader is ready and the lookup should run again.
const auto wait_for_shader = [this](const Program& program) {
if (!async_wait.Enabled() || !program.pending) {
return false;
}
const auto allowance = async_wait.Allowance();
if (allowance.count() <= 0) {
return false;
}
const auto started = std::chrono::steady_clock::now();
const bool finished = program.pending->job.WaitFor(allowance);
async_wait.Tally(std::chrono::steady_clock::now() - started, finished);
if (!finished) {
return false;
}
PublishShaders();
return !program.pending; // published; else the lookup would find it pending again
};
auto it_pgm = program_cache.find(params.hash); // this thread is the only writer
const bool compile_async = AsyncCompilation() && hw_stage != HwStage::Compute &&
!sel.regs->stage_enable.hs_en &&
@@ -1738,37 +1879,62 @@ PipelineCache::Result PipelineCache::GetProgram(PipelineSelection& sel, HwStage
}
if (it_pgm == program_cache.end()) {
auto new_program = std::make_unique<Program>(hw_stage, sw_stage, params);
const size_t first_idx = NewPermutationIndex(*new_program, params.hash);
if (compile_async) {
QueueShader(*new_program, hw_stage, sw_stage, params, runtime_info, binding, 0, true);
std::unique_lock lock{programs_mutex};
program_cache.emplace(params.hash, std::move(new_program));
QueueShader(*new_program, hw_stage, sw_stage, params, runtime_info, binding, first_idx,
true);
const Program* queued = new_program.get();
{
std::unique_lock lock{programs_mutex};
program_cache.emplace(params.hash, std::move(new_program));
}
if (wait_for_shader(*queued)) {
return GetProgram(sel, hw_stage, sw_stage, params, binding);
}
return {};
}
auto start = binding;
auto compile_runtime = runtime_info; // translation adjusts it; the key keeps the original
const auto module =
CompileModule(new_program->info, compile_runtime, params.code, 0, binding);
vk::ShaderModule module;
try {
module = CompileModule(new_program->info, compile_runtime, params.code, first_idx,
binding);
} catch (const Shader::UnsupportedShaderError& e) {
// gow3: kept in the cache as failed, so its draws are skipped without retranslating.
LOG_ERROR(Render_Vulkan, "gow3: shader {:#x} refused: {}", params.hash, e.what());
new_program->compilation_failed = true;
std::unique_lock lk{programs_mutex};
program_cache.emplace(params.hash, std::move(new_program));
return {};
}
auto spec = Shader::StageSpecialization(new_program->info, runtime_info, profile, start);
const auto perm_hash = HashCombine(params.hash, 0);
const auto perm_hash = HashCombine(params.hash, first_idx);
RegisterShaderMeta(new_program->info, spec.fetch_shader_data, spec, perm_hash, 0);
new_program->AddPermut(module, std::move(spec));
RegisterShaderMeta(new_program->info, spec.fetch_shader_data, spec, perm_hash, first_idx);
new_program->InsertPermut(module, std::move(spec), first_idx);
new_program->info_template = std::make_unique<Shader::Info>(new_program->info);
Program* program = new_program.get();
{
std::unique_lock lk{programs_mutex};
program_cache.emplace(params.hash, std::move(new_program));
}
return std::make_tuple(&program->info, module, program->modules[0].spec.fetch_shader_data,
perm_hash);
return std::make_tuple(&program->info, module,
program->modules[first_idx].spec.fetch_shader_data, perm_hash);
}
auto& program = it_pgm.value();
if (program->compilation_failed || (program->modules.empty() && program->pending)) {
if (!program->compilation_failed && wait_for_shader(*program)) {
return GetProgram(sel, hw_stage, sw_stage, params, binding);
}
return {};
}
if (program->modules.empty() && compile_async) {
QueueShader(*program, hw_stage, sw_stage, params, runtime_info, binding, 0, true);
QueueShader(*program, hw_stage, sw_stage, params, runtime_info, binding,
NewPermutationIndex(*program, params.hash), true);
if (wait_for_shader(*program)) {
return GetProgram(sel, hw_stage, sw_stage, params, binding);
}
return {};
}
if (!program->info_template) {
@@ -1780,6 +1946,74 @@ PipelineCache::Result PipelineCache::GetProgram(PipelineSelection& sel, HwStage
info.pgm_base = params.Base(); // Needs to be actualized for inline cbuffer address fixup
info.user_data = params.user_data;
info.RefreshFlatBuf();
// tlg (i3-pc 0120115): the specialization depends only on the flattened user data (which holds
// every register, so the fetch shader address too), the runtime info, the first bindings and,
// for a vertex shader, the vertex buffer descriptors its fetch shader reads from guest memory.
// Tessellation stages also read constants from memory: never reused. GOW3_VARIANT_REUSE=1
// turns this on; =check also builds the specialization on every hit and logs a mismatch.
static const int reuse_mode = [] {
const char* value = std::getenv("GOW3_VARIANT_REUSE");
return !value ? 0 : (std::string_view{value} == "check" ? 2 : (value[0] == '1' ? 1 : 0));
}();
static std::atomic<u64> reuse_hits{0}, reuse_misses{0}, reuse_mismatches{0};
auto& last = program->last_match;
const bool reusable = reuse_mode != 0 && !Gow3Toggle::Disabled(Gow3Toggle::VariantReuse) &&
sw_stage != SwStage::TessellationControl &&
sw_stage != SwStage::TessellationEval;
const auto vs_sharps_match = [&](bool store) {
const auto& fetch = program->modules[last.perm_idx].spec.fetch_shader_data;
if (!fetch) {
if (store) {
last.vs_sharps.clear();
}
return store || last.vs_sharps.empty();
}
if (store) {
last.vs_sharps.clear();
} else if (last.vs_sharps.size() != fetch->attributes.size()) {
return false;
}
for (size_t i = 0; i < fetch->attributes.size(); ++i) {
const auto sharp = fetch->attributes[i].GetSharp(info);
std::array<u32, 4> words;
static_assert(sizeof(sharp) == sizeof(words));
std::memcpy(words.data(), &sharp, sizeof(words));
if (store) {
last.vs_sharps.push_back(words);
} else if (last.vs_sharps[i] != words) {
return false;
}
}
return true;
};
if (reusable && last.perm_idx < program->modules.size() && last.start == binding &&
last.runtime_info == runtime_info && last.flat_buf == info.flattened_ud_buf &&
vs_sharps_match(false)) {
auto& hit = program->modules[last.perm_idx];
if (reuse_mode == 2) {
// The lookup below compares stored == fresh (the operator is not symmetric).
const auto check = Shader::StageSpecialization(info, runtime_info, profile, binding);
if (!(hit.spec == check)) {
if (reuse_mismatches.fetch_add(1) < 16) {
LOG_ERROR(Render_Vulkan,
"gow3: variant reuse of shader {:#x} perm {} differs from a fresh "
"specialization: {}",
params.hash, last.perm_idx, DescribeSpecDiff(check, hit.spec));
}
}
}
++reuse_hits;
program->last_used = last.perm_idx;
info.AddBindings(binding);
return std::make_tuple(&program->info, hit.module, hit.spec.fetch_shader_data,
HashCombine(params.hash, last.perm_idx));
}
if (reusable && ((++reuse_misses) & 0xffff) == 0) {
LOG_INFO(Render_Vulkan, "gow3: variant reuse: {} hits, {} misses, {} mismatches",
reuse_hits.load(), reuse_misses.load(), reuse_mismatches.load());
}
const auto start_binding = binding;
auto spec = Shader::StageSpecialization(info, runtime_info, profile, binding);
size_t perm_idx = program->modules.size();
@@ -1807,23 +2041,40 @@ PipelineCache::Result PipelineCache::GetProgram(PipelineSelection& sel, HwStage
program->failed_permutations.end()) {
return {};
}
perm_idx = NewPermutationIndex(*program, params.hash);
perm_hash = HashCombine(params.hash, perm_idx);
if (compile_async) {
QueueShader(*program, hw_stage, sw_stage, params, runtime_info, binding, perm_idx, false,
std::move(spec));
return {};
}
auto new_info = Shader::Info(hw_stage, sw_stage, params);
module = CompileModule(new_info, runtime_info, params.code, perm_idx, binding);
try {
module = CompileModule(new_info, runtime_info, params.code, perm_idx, binding);
} catch (const Shader::UnsupportedShaderError& e) {
LOG_ERROR(Render_Vulkan, "gow3: shader {:#x}/{} refused: {}", params.hash, perm_idx,
e.what());
spec.info = &program->info;
program->failed_permutations.push_back(std::move(spec));
return {};
}
RegisterShaderMeta(info, spec.fetch_shader_data, spec, perm_hash, perm_idx);
std::unique_lock lk{programs_mutex};
program->AddPermut(module, std::move(spec));
program->InsertPermut(module, std::move(spec), perm_idx);
} else {
info.AddBindings(binding);
module = it->module;
perm_idx = std::distance(program->modules.begin(), it);
perm_hash = HashCombine(params.hash, perm_idx);
}
if (reusable && perm_idx < program->modules.size()) {
last.perm_idx = perm_idx;
last.flat_buf = info.flattened_ud_buf;
last.runtime_info = runtime_info;
last.start = start_binding;
vs_sharps_match(true);
}
return std::make_tuple(&program->info, module,
program->modules[perm_idx].spec.fetch_shader_data, perm_hash);
}
@@ -55,6 +55,15 @@ struct Program {
Shader::Info info;
ModuleList modules{};
size_t last_used = 0; ///< gow3: permutation of the previous lookup, compared first
/// tlg (i3-pc 0120115): the inputs of the last variant match. A draw that brings the same
/// ones again takes the same variant without building its specialization (GetProgram).
struct LastMatch {
size_t perm_idx = SIZE_MAX;
std::vector<u32> flat_buf;
Shader::RuntimeInfo runtime_info{};
Shader::Backend::Bindings start{};
boost::container::small_vector<std::array<u32, 4>, 16> vs_sharps;
} last_match;
/// gow3: `info` as translated, for draw-preparation workers (they must not read `info`,
/// whose user data the GPU thread rewrites every draw). Guarded by programs_mutex.
std::unique_ptr<Shader::Info> info_template;
@@ -73,6 +82,8 @@ struct Program {
/// gow3: at 16, 32, 64... permutations, logs which key fields differ between the last two
/// (a field that changes every draw makes the list, and every lookup, grow without end).
void LogPermutationGrowth() const;
/// gow3: the pending compilation is the program's first: `info` is not translated yet.
[[nodiscard]] bool PendingInitial() const;
void InsertPermut(vk::ShaderModule module, Shader::StageSpecialization&& spec,
size_t perm_idx) {
@@ -197,6 +208,8 @@ private:
bool SaveDriverCacheUnchecked();
void SeedWorkerCaches();
bool CheckCacheProfile();
/// gow3: index for a new permutation of `program`: past every index the cache on disk holds.
size_t NewPermutationIndex(const Program& program, u64 hash) const;
struct BackgroundWarmup;
struct WarmStage;
void ReadWarmEntry(BackgroundWarmup& warm, std::vector<u8>&& data);
@@ -247,6 +260,12 @@ private:
BackgroundWarmup* background = nullptr;
tsl::robin_map<GraphicsPipelineKey, std::shared_ptr<AsyncGraphicsCompilation>> pending_graphics;
tsl::robin_map<size_t, std::unique_ptr<Program>> program_cache;
/// gow3: per program hash, one past the highest permutation index of the .spv files on disk.
/// A permutation compiled in game takes an index from here, never one that a cached
/// permutation not yet loaded (background warm-up) already names: its .meta would be
/// overwritten with another specialization, and the .key files that name it would build
/// their pipelines with the wrong shader in the next session. Filled by CheckCacheProfile.
std::unordered_map<u64, size_t> next_permutation;
std::vector<Program*> pending_programs;
/// gow3: exclusive for program/permutation insertions, shared for worker lookups.
std::shared_mutex programs_mutex;
@@ -771,6 +771,8 @@ void GraphicsPipeline::SerializationSupport::Serialize(Serialization::Archive& a
sdata.Write(multisampling);
sdata.Write(tcs);
sdata.Write(tes);
// gow3: last, so .key files written before it still load (with no discard shader).
sdata.Write(fragment);
}
bool GraphicsPipeline::SerializationSupport::Deserialize(Serialization::Archive& ar) {
@@ -782,6 +784,9 @@ bool GraphicsPipeline::SerializationSupport::Deserialize(Serialization::Archive&
sdata.Read(multisampling);
sdata.Read(tcs);
sdata.Read(tes);
if (!ar.IsEoS()) {
sdata.Read(fragment);
}
return true;
}
@@ -892,6 +897,7 @@ bool PipelineCache::LoadPipelineStage(Serialization::Archive& ar, size_t stage)
}
bool PipelineCache::CheckCacheProfile() {
next_permutation.clear();
if (!EmulatorSettings.IsPipelineCacheEnabled()) {
return false;
}
@@ -931,9 +937,39 @@ bool PipelineCache::CheckCacheProfile() {
std::move(profile_data));
return false;
}
// gow3: the indices the cache already holds, so permutations compiled in game never take one.
Storage::DataBase::Instance().ForEachBlobName(
Storage::BlobType::ShaderBinary, [this](std::string_view name) {
// "{:#018x}_{}": program hash, permutation index
const auto split = name.find('_');
if (split == std::string_view::npos) {
return;
}
const std::string hash_text{name.substr(0, split)};
const std::string index_text{name.substr(split + 1)};
char* end = nullptr;
const u64 hash = std::strtoull(hash_text.c_str(), &end, 16);
if (end == hash_text.c_str() || *end) {
return;
}
const u64 index = std::strtoull(index_text.c_str(), &end, 10);
if (end == index_text.c_str() || *end) {
return;
}
auto& next = next_permutation[hash];
next = std::max<size_t>(next, index + 1);
});
std::printf("Pipeline cache: %zu programs on disk\n", next_permutation.size());
return true;
}
size_t PipelineCache::NewPermutationIndex(const Program& program, u64 hash) const {
// A program has at most one permutation in flight, and the one inserted at this index makes
// modules.size() the next one, so nothing needs reserving (a refused queue retries the same).
const auto it = next_permutation.find(hash);
return std::max(program.modules.size(), it == next_permutation.end() ? 0 : it->second);
}
void PipelineCache::WarmUp(const std::function<void(u32, u32)>& progress) {
if (!CheckCacheProfile()) {
return;
@@ -1081,6 +1117,7 @@ struct PipelineCache::BackgroundWarmup {
u32 batches = 0;
bool finished = false;
u32 loaded = 0, threads = 0;
u32 merge_refused_initial = 0, merge_refused_moved = 0, merge_refused_taken = 0;
std::chrono::steady_clock::time_point started, last_batch;
std::chrono::steady_clock::time_point last_pump{}, last_report{};
std::unique_ptr<WarmupPool> pool; ///< last: its jobs use the members above
@@ -1205,17 +1242,22 @@ bool PipelineCache::MergeWarmStage(WarmStage& stage) {
}
// The game already uses this shader: its permutations are the ones that count.
Program& program = *it_pgm->second;
if (program.pending) {
return false; // the game is compiling a permutation that takes the next index
if (program.PendingInitial()) {
++background->merge_refused_initial; // its Info is not translated yet
return false;
}
// A permutation the game compiles takes an index past every cached one
// (NewPermutationIndex), so a cached permutation keeps its own even while one is pending.
const auto it = std::ranges::find(program.modules, stage.spec, &Program::Module::spec);
if (it != program.modules.end()) {
if (size_t(std::distance(program.modules.begin(), it)) != stage.perm_idx) {
++background->merge_refused_moved; // the game compiled it again at another index
return false;
}
stage.result = it->module;
} else {
if (stage.perm_idx < program.modules.size() && program.modules[stage.perm_idx].module) {
++background->merge_refused_taken; // another specialization holds this index
return false;
}
stage.spec.info = &program.info;
@@ -1337,6 +1379,9 @@ void PipelineCache::PumpWarmUp() {
warm.loaded, warm.total.load(),
std::chrono::duration<double>(now - warm.started).count(), warm.threads);
LOG_INFO(Render, "Preloaded {} pipelines in the background", warm.loaded);
std::printf("Warm-up merge: refused %u (initial compile pending), %u (compiled again at "
"another index), %u (index taken by another specialization)\n",
warm.merge_refused_initial, warm.merge_refused_moved, warm.merge_refused_taken);
ReportPipelineBinaryCache();
std::printf("Warm-up profile: reader wall %.3f s, entries %.3f s, shader IO/meta %.3f s, "
"modules %.3f s (%u); GPU pump %.3f s, max %.3f ms, gap max %.3f ms, "
@@ -6,18 +6,23 @@
#ifndef _WIN32
#include <array>
#include <atomic>
#include <filesystem>
#include <mutex>
#include <optional>
#include <dlfcn.h>
#endif
#include <cmath>
#include <unordered_set>
#include <xxhash.h>
#include "video_core/renderer_vulkan/ui_composition.h"
#include "gow3_toggles.h"
#include "gow3_settings.h"
#include "video_core/renderer_vulkan/vk_frame_capture.h"
#include "video_core/renderer_vulkan/vk_lighting_test.h"
#include "video_core/renderer_vulkan/vk_pass_census.h"
#include "video_core/renderer_vulkan/vk_frame_record.h"
#include "common/backports.h"
#include "video_core/cache_storage.h"
#include "common/debug.h"
#include "common/hash.h"
#include "core/debug_state.h"
@@ -1272,6 +1277,37 @@ void Rasterizer::DrawIndirect(bool is_indexed, VAddr arg_address, u32 offset, u3
}
}
// tlg (i3-pc 7036810): arguments that no real draw comes near make the GPU hang until the
// driver resets the device (an instance count in the billions from unrelated float data). Every
// slot of a multi draw is checked, not just the first, when the arguments live in plain guest
// memory; slots the GPU wrote (compute) are not in guest memory yet and are left to the GPU.
// GOW3_INDIRECT_CHECK=0 turns the check off. The i3-pc also draws from the checked copy even for
// GPU-written slots (particle counts a frame old): not ported, it changes what is drawn.
static const bool check_args = [] {
const char* value = std::getenv("GOW3_INDIRECT_CHECK");
return !value || value[0] != '0';
}();
if (check_args && count_address == 0 && stride >= 16 && max_count != 0 &&
!buffer_cache.IsRegionGpuModified(arg_address + offset, u64(stride) * max_count)) {
constexpr u32 max_elements = 0x400000;
constexpr u32 max_instances = 0x10000;
constexpr u32 max_slots = 256;
for (u32 slot = 0; slot < std::min(max_count, max_slots); ++slot) {
std::array<u32, 2> args{};
memory->CopySparseMemory(arg_address + offset + u64(slot) * stride,
reinterpret_cast<u8*>(args.data()), sizeof(args));
if (args[0] > max_elements || args[1] > max_instances) {
static std::atomic<u32> dropped{0};
if (dropped.fetch_add(1, std::memory_order_relaxed) < 32) {
LOG_WARNING(Render,
"gow3: dropped indirect draw at {:#x} slot {}: count {:#x}, "
"instances {:#x}",
arg_address + offset, slot, args[0], args[1]);
}
return;
}
}
}
const DrawIndirectParams params = {
.vertex_sgpr_offset = vertex_sgpr_offset,
.instance_sgpr_offset = instance_sgpr_offset,
@@ -1827,6 +1863,80 @@ void Rasterizer::Finish() {
scheduler.Finish();
}
// tlg (i3-pc 25f247c): a game that binds a heap whole and read-only (inFAMOUS: 0.5 and 2.5 GB
// geometry heaps, indexed by a per-instance offset, so no part can be left out) uploaded all of it
// in the frame of the first bind (a freeze of 2 to 3 seconds). The ranges are remembered per title
// (heap_binds.txt in the game cache) and, in later sessions, uploaded a slice per submission until
// the game binds them, so the first bind finds only what was written since. Uploading early is
// sound: a later write marks the pages again. On; GOW3_HEAP_WARM=0 turns it off.
namespace {
constexpr u64 HugeBindSize = 256ull << 20;
constexpr u64 HugeWarmSlice = 16ull << 20;
struct HugeBind {
VAddr base;
u64 size;
u64 cursor{};
bool bound{};
};
bool HugeWarmEnabled() {
static const bool enabled = [] {
const char* value = std::getenv("GOW3_HEAP_WARM");
return !value || value[0] != '0';
}();
return enabled;
}
std::filesystem::path HugeBindFile() {
return Storage::GameCacheDir() / "heap_binds.txt";
}
std::mutex huge_binds_mutex;
std::vector<HugeBind>& HugeBinds() {
static std::vector<HugeBind> binds = [] {
std::vector<HugeBind> loaded;
if (FILE* f = std::fopen(HugeBindFile().string().c_str(), "r")) {
unsigned long long base, size;
while (loaded.size() < 16 && std::fscanf(f, "%llx %llx", &base, &size) == 2) {
if (size >= HugeBindSize && size <= (4ull << 30)) {
loaded.push_back({VAddr(base), u64(size)});
}
}
std::fclose(f);
}
return loaded;
}();
return binds;
}
void NoteHugeBind(VAddr base, u64 size) {
if (!HugeWarmEnabled()) {
return;
}
std::scoped_lock lock{huge_binds_mutex};
auto& binds = HugeBinds();
for (auto& bind : binds) {
if (bind.base == base && bind.size == size) {
bind.bound = true;
return;
}
}
if (binds.size() >= 16) {
return;
}
binds.push_back({base, size, 0, true});
std::error_code ec;
std::filesystem::create_directories(HugeBindFile().parent_path(), ec);
if (FILE* f = std::fopen(HugeBindFile().string().c_str(), "a")) {
std::fprintf(f, "%llx %llx\n", static_cast<unsigned long long>(base),
static_cast<unsigned long long>(size));
std::fclose(f);
}
}
} // namespace
void Rasterizer::OnSubmit() {
DrainDrawPipe();
if (fault_process_pending) {
@@ -1835,6 +1945,28 @@ void Rasterizer::OnSubmit() {
}
texture_cache.ProcessDownloadImages();
texture_cache.RunGarbageCollector();
if (HugeWarmEnabled()) {
// One slice of each remembered heap this session has not bound yet. The draw pipe was
// drained above, so no draw is binding buffers meanwhile.
std::scoped_lock lock{huge_binds_mutex};
for (auto& bind : HugeBinds()) {
if (bind.bound) {
continue;
}
const VAddr start = bind.base + bind.cursor;
const u64 length = std::min(HugeWarmSlice, bind.size - bind.cursor);
bind.cursor = bind.cursor + length >= bind.size ? 0 : bind.cursor + length;
boost::container::small_vector<std::pair<VAddr, u64>, 8> parts;
ForEachMappedRangeInRange(start, length, [&](const auto& range) {
parts.emplace_back(range.lower(), range.upper() - range.lower());
});
for (const auto& [addr, part_size] : parts) {
if (part_size > 64_KB) {
(void)buffer_cache.ObtainBuffer(addr, u32(part_size), false);
}
}
}
}
runtime.TickFrame();
}
@@ -1964,11 +2096,14 @@ bool Rasterizer::BindResources(const Pipeline* pipeline) {
// gow3: G-buffer passes sample material textures for a scene rendered below the output
// size; the temporal upscaler restores the detail of the output's mip level (FSR guide:
// log2(render / output)). Shadows, post-processing and UI keep the guest's bias.
sampler_lod_bias = 0.0f;
// The menu's texture LOD bias (mip_lod_bias) adds to every draw: textures without mips
// (UI, full-screen passes) are unaffected. Steps of 1/8: dragging the slider creates few samplers.
sampler_lod_bias = pipeline->IsCompute() ? 0.0f
: std::round(Gow3Settings::Get().mip_lod_bias.load() * 8.0f) / 8.0f;
if (!pipeline->IsCompute() && !Gow3Toggle::Disabled(Gow3Toggle::SceneMipBias) &&
std::popcount(static_cast<const GraphicsPipeline*>(pipeline)->GetGraphicsKey().mrt_mask &
0xff) >= 5) {
sampler_lod_bias = upscaler->SceneMipBias();
sampler_lod_bias += upscaler->SceneMipBias();
static float reported = 0.0f;
if (sampler_lod_bias != reported) {
reported = sampler_lod_bias;
@@ -2578,6 +2713,9 @@ void Rasterizer::BindBuffers(const Shader::Info& stage, const PreparedStage* pre
vsharp.GetSize(), size, stage.pgm_hash);
}
}
if (!desc.is_written && size >= HugeBindSize) {
NoteHugeBind(vsharp.base_address, size);
}
const auto [buffer, offset] = buffer_cache.ObtainBuffer(
vsharp.base_address, size, desc.is_written, desc.is_formatted);
const u64 offset_aligned = Common::AlignDown(offset, alignment);
@@ -2617,7 +2755,9 @@ Rasterizer::ImageDescCacheEntry& Rasterizer::CachedImageDescEntry(const AmdGpu::
u64 hash) {
std::array<u64, 4> key;
std::memcpy(key.data(), &sharp, sizeof(key));
const u32 flags = u32(res.is_written) | u32(res.is_depth) << 1 | u32(res.is_array) << 2;
// tlg (gow3_pc #348): needs_native too, since FindImage resolves partial height views only for it.
const u32 flags = u32(res.is_written) | u32(res.is_depth) << 1 | u32(res.is_array) << 2 |
u32(res.needs_native) << 3;
// Two-way set associative: a frame binds a few thousand distinct T#s.
auto* set = &image_desc_cache[(hash % (image_desc_cache.size() / 2)) * 2];
const auto matches = [&](const ImageDescCacheEntry& e) {
@@ -3,8 +3,12 @@
#pragma once
#include <chrono>
#include <source_location>
#include "common/recursive_lock.h"
#include "common/shared_first_mutex.h"
#include "video_core/renderer_vulkan/vk_wait_diagnostics.h"
#include "video_core/buffer_cache/buffer_cache.h"
#include "video_core/page_manager.h"
#include "video_core/renderer_vulkan/vk_bind_helper.h"
@@ -92,8 +96,19 @@ public:
u32 ReadDataFromGds(u32 gsd_offset);
bool InvalidateMemory(VAddr addr, u64 size, bool assume_locks = false);
/// GPU thread, before a write the guest can observe (see Scheduler::WaitHostCopies).
void WaitHostCopies() {
/// gow3: with GOW3_PERF_DIAG=1 the time waited is attributed to the calling line (the report of
/// WaitDiagnostics), to see which guest-visible write holds the GPU thread for the copies.
void WaitHostCopies(std::source_location where = std::source_location::current()) {
DrainDrawPipe();
if (WaitDiagnostics::Enabled()) {
const auto start = std::chrono::steady_clock::now();
scheduler.WaitHostCopies();
WaitDiagnostics::Get().Add(
where.file_name(), where.line(), 0,
std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now() - start).count());
return;
}
scheduler.WaitHostCopies();
}
@@ -25,17 +25,19 @@ public:
entries.push_back({address, size, tick, std::move(payload)});
bytes += size;
}
std::uint64_t RequiredTick(std::uint64_t address, std::uint64_t size) const {
/// `exact`: only copies sharing bytes with the range, not just a page (gow3_pc #386: for
/// consumers that read or drop exactly those bytes, e.g. an image upload).
std::uint64_t RequiredTick(std::uint64_t address, std::uint64_t size, bool exact = false) const {
std::uint64_t tick = 0;
for (const auto& entry : entries) {
if (entry.valid && entry.watched && OverlapsPage(entry, address, size)) tick = entry.tick;
if (entry.valid && entry.watched && Overlaps(entry, address, size, exact)) tick = entry.tick;
}
return tick;
}
template <typename Callback>
void Cancel(std::uint64_t address, std::uint64_t size, Callback&& cancel) {
void Cancel(std::uint64_t address, std::uint64_t size, Callback&& cancel, bool exact = false) {
for (auto& entry : entries) {
if (entry.valid && OverlapsPage(entry, address, size)) {
if (entry.valid && Overlaps(entry, address, size, exact)) {
cancel(entry);
entry.valid = false;
}
@@ -76,6 +78,10 @@ private:
const auto end = (entry.address + entry.size + 4095) & ~std::uint64_t{4095};
return address < end && begin < address + size;
}
static bool Overlaps(const Entry& entry, std::uint64_t address, std::uint64_t size, bool exact) {
if (!exact) return OverlapsPage(entry, address, size);
return size && address < entry.address + entry.size && entry.address < address + size;
}
static constexpr std::uint64_t MaxBytes = 64ull << 20;
std::deque<Entry> entries;
std::uint64_t bytes = 0;
@@ -25,6 +25,7 @@
#include "video_core/texture_cache/host_compatibility.h"
#include "video_core/texture_cache/texture_cache.h"
#include "video_core/texture_cache/tile_manager.h"
#include <vulkan/vulkan_format_traits.hpp>
namespace VideoCore {
@@ -129,6 +130,12 @@ void TextureCache::RetireReadbacks() {
bool TextureCache::ResolveReadbacks(VAddr address, u64 size, bool assume_locks,
ReadbackSource source) {
if (!deferred_readbacks || !has_pending_readbacks.load(std::memory_order_acquire)) return false;
// tlg (gow3_pc #386): image uploads read, and frees drop, exactly the image's bytes. A copy of
// a neighboring image that only shares a page is published later over other bytes. Only with
// stale readbacks: otherwise pending pages are read-protected and the copy threads fault.
const bool exact = ExactReadbackWaits() &&
(source == ReadbackSource::ImageRefresh || source == ReadbackSource::BufferForImage ||
source == ReadbackSource::ImageFree);
{
std::scoped_lock lock{readback_mutex};
// Stale mode, byte rule: God of War III writes every frame to the page that holds a
@@ -138,9 +145,12 @@ bool TextureCache::ResolveReadbacks(VAddr address, u64 size, bool assume_locks,
readbacks_released += pending_readbacks.ReleaseOutside(address, size, 64_KB,
[this](auto& entry) { WatchReadback<false>(entry.address, entry.size); });
}
if (!pending_readbacks.RequiredTick(address, size)) return false;
if (!pending_readbacks.RequiredTick(address, size, exact)) {
if (exact && pending_readbacks.RequiredTick(address, size)) ++readbacks_neighbors;
return false;
}
}
const auto resolve = [this, address, size, source] {
const auto resolve = [this, address, size, source, exact] {
const bool diag = Vulkan::WaitDiagnostics::Enabled();
std::scoped_lock lock{readback_mutex};
bool waited = false;
@@ -154,7 +164,7 @@ bool TextureCache::ResolveReadbacks(VAddr address, u64 size, bool assume_locks,
}
}
Gow3Stats::Timer readback_timer{Gow3Stats::t_readback_wait};
if (const u64 tick = pending_readbacks.RequiredTick(address, size)) {
if (const u64 tick = pending_readbacks.RequiredTick(address, size, exact)) {
++readbacks_waited;
waited = !scheduler.IsFree(tick);
const auto start = diag ? std::chrono::steady_clock::now()
@@ -174,18 +184,86 @@ bool TextureCache::ResolveReadbacks(VAddr address, u64 size, bool assume_locks,
return true;
}
void TextureCache::CancelReadbacks(VAddr address, u64 size) {
void TextureCache::CancelReadbacks(VAddr address, u64 size, bool exact) {
if (!deferred_readbacks) return;
std::scoped_lock lock{readback_mutex};
pending_readbacks.Cancel(address, size, [this](auto& entry) {
if (entry.watched) WatchReadback<false>(entry.address, entry.size);
++readbacks_canceled;
});
}, exact);
}
// gow3: every copy is recorded first and the GPU is waited for once. A wait per image (as
// before) left the GPU mostly idle in God of War III's gameplay, at about 2 FPS.
// tlg (GOW3_PERF_DIAG, from gow3_pc #348): a request PartialHeightViews answered with a taller
// cached image.
void TextureCache::NotePartialView(const ImageInfo& request, const ImageInfo& used,
BindingType binding) {
++partial_views;
const auto same = [&](const PartialViewStat& s) {
return s.address == request.guest_address && s.format == request.pixel_format &&
s.used_format == used.pixel_format && s.used_height == used.size.height &&
s.binding == binding;
};
auto slot = std::ranges::find_if(partial_view_stats, same);
if (slot == partial_view_stats.end()) {
slot = std::ranges::min_element(partial_view_stats, {}, &PartialViewStat::count);
*slot = {request.guest_address, request.pixel_format, used.pixel_format, used.size.width,
used.size.height, request.size.height, request.size.height, 0, binding};
}
slot->min_height = std::min(slot->min_height, request.size.height);
slot->max_height = std::max(slot->max_height, request.size.height);
++slot->count;
}
// tlg (GOW3_PERF_DIAG, from gow3_pc #348): every 5 s, the images FindImage created most often.
// Takes `mutex` (held by FindImage), so it runs before ProcessDownloadImages takes readback_mutex.
void TextureCache::ReportImageCreates() {
const auto now = std::chrono::steady_clock::now();
if (now - create_report < std::chrono::seconds(5)) {
return;
}
create_report = now;
std::scoped_lock lock{mutex};
static constexpr const char* bindings[] = {"texture", "storage", "render target",
"depth target", "video out"};
std::printf("Image creates: %llu in 5 s\n", static_cast<unsigned long long>(images_created));
std::ranges::sort(create_stats, std::greater{}, &CreateStat::count);
for (u32 i = 0; i < 8 && create_stats[i].count; ++i) {
const auto& s = create_stats[i];
std::printf(" %u x %#llx %ux%u %s pitch %u tile %u as %s%s: %s", s.count,
static_cast<unsigned long long>(s.address), s.width, s.height,
vk::to_string(s.format).c_str(), s.pitch, s.tile,
bindings[std::min<u32>(u32(s.binding), 4)], s.needs_native ? "" : " (sampled)",
s.reason);
if (s.overlaps) {
std::printf(" (%u cached; was %#llx %ux%u %s pitch %u tile %u)", s.overlaps,
static_cast<unsigned long long>(s.prev_address), s.prev_width,
s.prev_height, vk::to_string(s.prev_format).c_str(), s.prev_pitch,
s.prev_tile);
}
std::printf("\n");
}
create_stats = {};
images_created = 0;
std::printf("Partial height views: %llu in 5 s\n",
static_cast<unsigned long long>(partial_views));
std::ranges::sort(partial_view_stats, std::greater{}, &PartialViewStat::count);
for (u32 i = 0; i < 8 && partial_view_stats[i].count; ++i) {
const auto& s = partial_view_stats[i];
std::printf(" %u x %#llx %s rows %u-%u as %s, on %ux%u %s\n", s.count,
static_cast<unsigned long long>(s.address), vk::to_string(s.format).c_str(),
s.min_height, s.max_height, bindings[std::min<u32>(u32(s.binding), 4)],
s.used_width, s.used_height, vk::to_string(s.used_format).c_str());
}
partial_view_stats = {};
partial_views = 0;
}
void TextureCache::ProcessDownloadImages() {
if (Vulkan::WaitDiagnostics::Enabled()) {
ReportImageCreates();
}
if (deferred_readbacks) {
// Upload workers must finish reading RAM before these pages become inaccessible.
scheduler.WaitHostCopies();
@@ -248,7 +326,10 @@ void TextureCache::ProcessDownloadImages() {
if (stale_readbacks) {
std::printf("Readback byte rule: %llu CPU writes went on without waiting\n",
static_cast<unsigned long long>(readbacks_released));
readbacks_released = 0;
std::printf("Readback exact waits: %llu image uploads/frees skipped a copy "
"on a shared page\n",
static_cast<unsigned long long>(readbacks_neighbors));
readbacks_released = readbacks_neighbors = 0;
}
std::ranges::sort(readback_images, std::greater{}, &ReadbackImageStat::copies);
for (u32 i = 0; i < 4 && readback_images[i].copies; ++i) {
@@ -570,6 +651,40 @@ void TextureCache::InvalidateMemory(VAddr addr, size_t size) {
});
}
void TextureCache::NoteBackingWrite(VAddr address, u64 size) {
if (!BackingInvalidateEnabled()) {
return;
}
std::scoped_lock lock{backing_writes_mutex};
backing_writes.emplace_back(address, size);
has_backing_writes.store(true, std::memory_order_release);
}
void TextureCache::ApplyBackingWrites() {
if (!has_backing_writes.load(std::memory_order_acquire)) {
return;
}
std::vector<std::pair<VAddr, u64>> writes;
{
std::scoped_lock lock{backing_writes_mutex};
writes.swap(backing_writes);
has_backing_writes.store(false, std::memory_order_release);
}
// God of War III streams textures with GPU copies through large storage images over the
// texture pool; their readbacks put the new texels in memory without a fault, and textures
// there kept the old contents until a restart.
for (const auto& [address, size] : writes) {
ForEachImageInRegion(address, size, [&](ImageId image_id, Image& image) {
// Images sharing memory with a GPU writer are synchronized by SynchronizeAlias.
if (True(image.flags & (ImageFlagBits::GpuModified | ImageFlagBits::Aliased))) {
return;
}
image.flags |= ImageFlagBits::CpuDirty;
UntrackImage(image_id);
});
}
}
void TextureCache::InvalidateMemoryFromGPU(VAddr address, size_t max_size) {
// Partial buffer writes still need the image's untouched bytes in guest memory.
ResolveReadbacks(address, max_size, true, ReadbackSource::GpuPartialWrite);
@@ -670,7 +785,8 @@ ImageId TextureCache::ResolveDepthOverlap(const ImageInfo& requested_info, Bindi
std::tuple<ImageId, int, int> TextureCache::ResolveOverlap(const ImageInfo& image_info,
BindingType binding,
ImageId cache_image_id,
ImageId merged_image_id) {
ImageId merged_image_id,
bool native_reads) {
auto& cache_image = slot_images[cache_image_id];
const bool safe_to_delete =
scheduler.CurrentTick() - cache_image.tick_accessed_last > NumFramesBeforeRemoval;
@@ -688,6 +804,43 @@ std::tuple<ImageId, int, int> TextureCache::ResolveOverlap(const ImageInfo& imag
if (image_info.guest_address == cache_image.info.guest_address) {
const u32 lhs_block_size = image_info.num_bits * image_info.guest_samples;
const u32 rhs_block_size = cache_image.info.num_bits * cache_image.info.guest_samples;
// tlg (gow3_pc #348): the top rows of a cached image with the same layout. God of War III
// reads its frame buffers as R8G8B8A8Uint with a height that changes every frame; as a
// "pool allocation" below, each read created and uploaded a new ~8 MB image (FPS 120 -> 40).
// - Only an integer view of another format: same-format requests here are real pool reuse
// (with them, normalized sampling read the wrong rows and blew out exposure).
// - Only for shaders that read with integer coordinates (ImageResource::needs_native).
// - Known limit: the view keeps the cached size, so accesses past the requested extent but
// inside the cached one reach the frame buffer instead of being dropped.
// Off unless GOW3_PARTIAL_HEIGHT_VIEWS=1: not measured in inFAMOUS yet (GOW3_PERF_DIAG=1
// prints 'Image creates' and 'Partial height views' every 5 s to check the pattern).
static const bool partial_views_enabled = [] {
const char* value = std::getenv("GOW3_PARTIAL_HEIGHT_VIEWS");
return value && value[0] == '1';
}();
if (partial_views_enabled && !Gow3Toggle::Disabled(Gow3Toggle::PartialHeightViews) &&
native_reads && (binding == BindingType::Texture || binding == BindingType::Storage)) {
const auto& a = image_info;
const auto& b = cache_image.info;
const auto is_integer = [](vk::Format format) {
const std::string_view numeric = vk::componentNumericFormat(format, 0);
return numeric == "UINT" || numeric == "SINT";
};
if (a.pixel_format != b.pixel_format && is_integer(a.pixel_format) &&
lhs_block_size == rhs_block_size && a.pitch == b.pitch &&
a.tile_mode == b.tile_mode && a.type == b.type && a.size.width <= b.size.width &&
a.size.height < b.size.height && a.size.depth == 1 && b.size.depth == 1 &&
a.resources.levels == 1 && a.resources.layers == 1 && b.resources.levels == 1 &&
b.resources.layers == 1 && !a.props.is_block && !b.props.is_block &&
!a.props.is_depth && !b.props.is_depth &&
IsVulkanFormatCompatible(b.pixel_format, a.pixel_format)) {
// Counted only on the request's own comparison (a is the merged image afterwards).
if (Vulkan::WaitDiagnostics::Enabled() && !merged_image_id) {
NotePartialView(a, b, binding);
}
return {cache_image_id, -1, -1};
}
}
if (image_info.BlockDim() != cache_image.info.BlockDim() ||
lhs_block_size != rhs_block_size) {
// Very likely this kind of overlap is caused by allocation from a pool.
@@ -933,7 +1086,7 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_fmt) {
u64 key_hash = info.guest_address ^ info.guest_size << 17 ^ u64(info.pixel_format) << 40 ^
u64(info.size.width) << 24 ^ info.size.height ^ u64(desc.type) << 58 ^
u64(exact_fmt) << 63;
u64(exact_fmt) << 63 ^ u64(desc.needs_native) << 62;
key_hash = (key_hash ^ key_hash >> 33) * 0xFF51AFD7ED558CCDull;
key_hash = (key_hash ^ key_hash >> 33) * 0xC4CEB9FE1A85EC53ull;
key_hash ^= key_hash >> 33;
@@ -955,6 +1108,7 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_fmt) {
cached.size == info.guest_size && cached.extent == info.size &&
cached.format == info.pixel_format && cached.type == info.type &&
cached.exact_fmt == exact_fmt && cached.binding == desc.type &&
cached.needs_native == desc.needs_native &&
cached.levels == info.resources.levels && cached.layers == info.resources.layers &&
!Gow3Toggle::Disabled(Gow3Toggle::FindImageCache)) {
Image& image = slot_images[cached.image_id];
@@ -1000,6 +1154,32 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_fmt) {
image_id = cache_id;
}
// tlg (GOW3_PERF_DIAG, from gow3_pc #348): what was cached here, read before overlap
// resolution may free it.
const bool diag = Vulkan::WaitDiagnostics::Enabled();
CreateStat created{};
if (diag) {
created = {info.guest_address, info.size.width, info.size.height, 1, info.pixel_format,
desc.type, image_ids.empty() ? "nothing cached" : "overlap unresolved"};
if (!image_id && !image_ids.empty()) {
const auto same = std::ranges::find_if(image_ids, [&](ImageId id) {
return slot_images[id].info.guest_address == info.guest_address;
});
const auto& prev =
slot_images[same != image_ids.end() ? *same : image_ids.front()].info;
created.prev_address = prev.guest_address;
created.prev_width = prev.size.width;
created.prev_height = prev.size.height;
created.prev_format = prev.pixel_format;
created.overlaps = static_cast<u32>(image_ids.size());
created.prev_pitch = prev.pitch;
created.prev_tile = static_cast<u32>(prev.tile_mode);
}
created.pitch = info.pitch;
created.needs_native = desc.needs_native;
created.tile = static_cast<u32>(info.tile_mode);
}
// Try to resolve overlaps (if any)
int view_mip{-1};
int view_slice{-1};
@@ -1010,7 +1190,7 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_fmt) {
const auto& merged_info = image_id ? slot_images[image_id].info : info;
auto [overlap_image_id, overlap_view_mip, overlap_view_slice] =
ResolveOverlap(merged_info, desc.type, cache_id, image_id);
ResolveOverlap(merged_info, desc.type, cache_id, image_id, desc.needs_native);
if (overlap_image_id) {
image_id = overlap_image_id;
view_mip = overlap_view_mip;
@@ -1024,17 +1204,35 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_fmt) {
if (exact_fmt && info.pixel_format != image_resolved.info.pixel_format) {
// Cannot reuse this image as we need the exact requested format.
image_id = {};
created.reason = "exact format";
} else if (image_resolved.info.resources < info.resources) {
// The image was clearly picked up wrong.
FreeImage(image_id);
image_id = {};
LOG_WARNING(Render_Vulkan, "Image overlap resolve failed");
created.reason = "fewer resources";
}
}
// Create and register a new image
if (!image_id) {
image_id = slot_images.insert(instance, runtime, slot_image_views, info);
RegisterImage(image_id);
if (diag) {
++images_created;
const auto same = [&](const CreateStat& s) {
return s.address == created.address && s.width == created.width &&
s.height == created.height && s.format == created.format &&
s.binding == created.binding;
};
auto slot = std::ranges::find_if(create_stats, same);
if (slot != create_stats.end()) {
created.count = slot->count + 1;
} else {
// A free slot, else the least seen one.
slot = std::ranges::min_element(create_stats, {}, &CreateStat::count);
}
*slot = created;
}
}
Image& image = slot_images[image_id];
@@ -1057,6 +1255,7 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_fmt) {
.type = info.type,
.exact_fmt = exact_fmt,
.binding = desc.type,
.needs_native = desc.needs_native,
.levels = info.resources.levels,
.layers = info.resources.layers,
.generation = registry_generation.load(std::memory_order_relaxed),
@@ -1716,6 +1915,10 @@ void TextureCache::RunGarbageCollector() {
SCOPE_EXIT {
++gc_tick;
};
if (has_backing_writes.load(std::memory_order_acquire)) {
std::scoped_lock lock{mutex};
ApplyBackingWrites();
}
GarbageCollectImages();
GarbageCollectSamplers();
@@ -1763,7 +1966,10 @@ void TextureCache::TouchImage(const Image& image) {
void TextureCache::DeleteImage(ImageId image_id) {
Image& image = slot_images[image_id];
CancelReadbacks(image.info.guest_address, image.info.guest_size);
// tlg (gow3_pc #386): only this image's bytes. FreeImage resolved its own copies; with
// ExactReadbackWaits, copies of neighbors that only share a page are still pending and must
// reach RAM.
CancelReadbacks(image.info.guest_address, image.info.guest_size, ExactReadbackWaits());
ASSERT_MSG(!image.IsTracked(), "Image was not untracked");
ASSERT_MSG(False(image.flags & ImageFlagBits::Registered), "Image was not unregistered");
@@ -79,11 +79,16 @@ public:
ImageInfo info;
ImageViewInfo view_info;
BindingType type{BindingType::Texture};
/// tlg (gow3_pc #348): the shader reads with integer coordinates
/// (ImageResource::needs_native), not normalized sampling. Gates ResolveOverlap's partial
/// height views.
bool needs_native{true};
ImageDesc() = default;
ImageDesc(const AmdGpu::Image& image, const Shader::ImageResource& desc)
: info{image, desc}, view_info{image, desc},
type{desc.is_written ? BindingType::Storage : BindingType::Texture} {}
type{desc.is_written ? BindingType::Storage : BindingType::Texture},
needs_native{desc.needs_native} {}
ImageDesc(const AmdGpu::ColorBuffer& buffer, AmdGpu::CbDbExtent hint)
: info{buffer, hint}, view_info{buffer}, type{BindingType::RenderTarget} {}
ImageDesc(const AmdGpu::DepthBuffer& buffer, AmdGpu::DepthView view,
@@ -119,6 +124,19 @@ public:
/// Invalidates any image in the logical page range.
void InvalidateMemory(VAddr addr, size_t size);
/// tlg (gow3_pc #331): guest memory written through the backing view (readbacks, buffer
/// downloads), which bypasses page tracking. Any lock may be held: images over the range are
/// marked CPU-dirty at the next UpdateImage or GC tick (ApplyBackingWrites). Only with
/// GOW3_BACKING_INVALIDATE=1.
void NoteBackingWrite(VAddr address, u64 size);
[[nodiscard]] static bool BackingInvalidateEnabled() {
static const bool enabled = [] {
const char* value = std::getenv("GOW3_BACKING_INVALIDATE");
return value && value[0] == '1';
}();
return enabled && !Gow3Toggle::Disabled(Gow3Toggle::BackingInvalidate);
}
/// Marks an image as dirty if it exists at the provided address.
void InvalidateMemoryFromGPU(VAddr address, size_t max_size);
@@ -129,7 +147,7 @@ public:
void ProcessDownloadImages();
bool ResolveReadbacks(VAddr address, u64 size, bool assume_locks = true,
ReadbackSource source = ReadbackSource::Unknown);
void CancelReadbacks(VAddr address, u64 size);
void CancelReadbacks(VAddr address, u64 size, bool exact = false);
/// Retrieves the image handle of the image with the provided attributes.
[[nodiscard]] ImageId FindImage(ImageDesc& desc, bool exact_fmt = false);
@@ -165,6 +183,7 @@ public:
constexpr u32 Registered = static_cast<u32>(ImageFlagBits::Registered);
constexpr u32 Aliased = static_cast<u32>(ImageFlagBits::Aliased);
return (flags & (Dirty | Registered | Aliased)) == Registered &&
!has_backing_writes.load(std::memory_order_acquire) &&
image.track_addr == image.guest_begin && image.track_addr_end == image.guest_end &&
image.lru_touched_tick == gc_tick;
}
@@ -184,12 +203,14 @@ public:
// Images sharing memory with another take the locked path: SynchronizeAlias.
constexpr u32 Aliased = static_cast<u32>(ImageFlagBits::Aliased);
if ((flags & (Dirty | Registered | Aliased)) == Registered &&
!has_backing_writes.load(std::memory_order_acquire) &&
image.track_addr == image.guest_begin && image.track_addr_end == image.guest_end &&
image.lru_touched_tick == gc_tick) {
return;
}
}
std::scoped_lock lock{mutex};
ApplyBackingWrites();
Image& image = slot_images[image_id];
TrackImage(image_id);
TouchImage(image);
@@ -206,7 +227,8 @@ public:
[[nodiscard]] std::tuple<ImageId, int, int> ResolveOverlap(const ImageInfo& info,
BindingType binding,
ImageId cache_img_id,
ImageId merged_image_id);
ImageId merged_image_id,
bool native_reads);
/// Resolves depth overlap and either re-creates the image or returns existing one
[[nodiscard]] ImageId ResolveDepthOverlap(const ImageInfo& requested_info, BindingType binding,
@@ -502,7 +524,39 @@ private:
u64 Bytes() const { return u64{width} * height * bits / 8; }
};
std::array<ReadbackImageStat, 16> readback_images{};
/// tlg (GOW3_PERF_DIAG, from gow3_pc #348): images FindImage had to create, by request, and the
/// image that was cached at that memory before (none: nothing overlapped).
struct CreateStat {
VAddr address;
u32 width, height, count;
vk::Format format;
BindingType binding;
const char* reason;
VAddr prev_address;
u32 prev_width, prev_height, overlaps;
vk::Format prev_format;
u32 pitch, tile, prev_pitch, prev_tile;
bool needs_native;
};
std::array<CreateStat, 16> create_stats{};
u64 images_created = 0;
std::chrono::steady_clock::time_point create_report = std::chrono::steady_clock::now();
void ReportImageCreates();
/// tlg (GOW3_PERF_DIAG): requests answered by Gow3Toggle::PartialHeightViews.
struct PartialViewStat {
VAddr address;
vk::Format format, used_format;
u32 used_width, used_height, min_height, max_height, count;
BindingType binding;
};
std::array<PartialViewStat, 16> partial_view_stats{};
u64 partial_views = 0;
void NotePartialView(const ImageInfo& request, const ImageInfo& used, BindingType binding);
u64 readbacks_released = 0;
u64 readbacks_neighbors = 0; ///< resolves ExactReadbackWaits let through (report)
bool ExactReadbackWaits() const {
return stale_readbacks && !Gow3Toggle::Disabled(Gow3Toggle::ExactReadbackWaits);
}
ReadbackQueue<Vulkan::StagingBufferRef> pending_readbacks;
std::mutex readback_mutex;
std::atomic<bool> has_pending_readbacks{false};
@@ -513,6 +567,11 @@ private:
void RetireReadbacks();
PageTable page_table;
std::mutex mutex;
/// tlg (gow3_pc #331): under `mutex`. GPU-modified images hold the written data themselves.
void ApplyBackingWrites();
std::mutex backing_writes_mutex;
std::vector<std::pair<VAddr, u64>> backing_writes;
std::atomic<bool> has_backing_writes{false};
/// gow3: images with the same guest address (guarded by `mutex`). `backing` holds the
/// complete contents; `writer` is a newer write not yet copied into it.
struct AliasState {
@@ -548,6 +607,7 @@ private:
AmdGpu::ImageType type{};
bool exact_fmt = false;
BindingType binding{};
bool needs_native = true;
u32 levels = 0;
u32 layers = 0;
u64 generation = ~0ULL;
+108
View File
@@ -0,0 +1,108 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "gow3_region.h"
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <mutex>
#include <string>
#include <unordered_set>
namespace Gow3Region {
namespace {
constexpr const char* TraceFileName = "region_trace.tsv";
constexpr const char* PackageSuffix = ".xpps";
/// The trace file and the set of pipelines already written. Events are rare (a package open, a
/// first pipeline request), so one mutex and a flush per line are cheap enough.
class Trace {
public:
static Trace& Get() {
static Trace instance;
return instance;
}
bool enabled() const { return file != nullptr; }
void Open(const std::string& package) {
Write("O\t%lld\t%s\n", Elapsed(), package.c_str());
}
void Pipeline(char kind, std::uint64_t hash, bool ready) {
std::scoped_lock lock{mutex};
if (!seen.insert(hash ^ (std::uint64_t(kind) << 56)).second) {
return;
}
std::fprintf(file, "P\t%lld\t%c\t%#llx\t%c\n", Elapsed(), kind, static_cast<unsigned long long>(hash),
ready ? 'r' : 'm');
std::fflush(file);
}
private:
Trace() {
const char* on = std::getenv("GOW3_REGION_TRACE");
if (!on || *on != '1') {
return;
}
const char* folder = std::getenv("GOW3_GPU_USER_DIR");
const std::filesystem::path path =
(folder && *folder ? std::filesystem::path(folder) : std::filesystem::current_path()) / TraceFileName;
file = std::fopen(path.string().c_str(), "w");
if (file) {
std::printf("Region trace: writing %s (GOW3_REGION_TRACE=1)\n", path.string().c_str());
}
}
~Trace() {
if (file) {
std::fclose(file);
}
}
long long Elapsed() const {
using namespace std::chrono;
return duration_cast<milliseconds>(steady_clock::now() - start).count();
}
template <typename... Args>
void Write(const char* format, Args... args) {
std::scoped_lock lock{mutex};
std::fprintf(file, format, args...);
std::fflush(file);
}
std::FILE* file = nullptr;
std::mutex mutex;
std::unordered_set<std::uint64_t> seen;
const std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
};
bool IsPackage(const char* path) {
const std::size_t length = std::strlen(path);
const std::size_t suffix = std::strlen(PackageSuffix);
return length > suffix && std::strcmp(path + length - suffix, PackageSuffix) == 0;
}
} // namespace
bool Enabled() {
return Trace::Get().enabled();
}
void NoteOpen(const char* guest_path) {
if (!guest_path || !Enabled() || !IsPackage(guest_path)) {
return;
}
const char* name = std::strrchr(guest_path, '/');
Trace::Get().Open(name ? name + 1 : guest_path);
}
void NotePipeline(char kind, std::uint64_t hash, bool ready) {
if (Enabled()) {
Trace::Get().Pipeline(kind, hash, ready);
}
}
} // namespace Gow3Region
+26
View File
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// gow3: region trace (GOW3_REGION_TRACE=1). inFAMOUS streams the city in `.xpps` packages; which
// package the game opens is the best hint of where the player is going. This writes one
// tab-separated line per event to `region_trace.tsv` in the GPU user folder, so an offline
// pass can tell which pipelines each region needs:
// O <ms> <package> the game opened an .xpps package
// P <ms> <g|c> <hash> <r|m> the first request of a graphics or compute pipeline; r = it was
// ready (cache or an earlier build), m = missing: the draw waits or goes
// <ms> is a monotonic clock shared by both kinds of line. The first step of the per-region
// pre-warm (DESIGN_PREWARM_POR_MAPA.md): measure before building anything on top.
#pragma once
#include <cstdint>
namespace Gow3Region {
/// True when GOW3_REGION_TRACE=1; every call below is a no-op otherwise.
bool Enabled();
/// The game opened `guest_path`; only `.xpps` packages are recorded.
void NoteOpen(const char* guest_path);
/// The game asked for a pipeline (`kind` 'g' graphics, 'c' compute); `ready`: it already exists.
/// Logged once per hash, with the state of that first request.
void NotePipeline(char kind, std::uint64_t hash, bool ready);
} // namespace Gow3Region
+60 -1
View File
@@ -3,6 +3,7 @@
#include "gow3_settings.h"
#include "gow3_orbs.h"
#include "gow3_graphics.h"
#include "gow3_view.h"
#include <algorithm>
#include <cstdio>
@@ -78,6 +79,22 @@ void Set(Values& v, const std::string& key, const std::string& value) {
return;
}
}
if (key == "key_map") {
v.key_map = value;
return;
}
if (key == "lod_bands") {
// Six comma-separated distances; missing or bad entries keep the current value.
const char* p = value.c_str();
for (auto& band : v.lod_bands) {
char* end = nullptr;
const float b = std::strtof(p, &end);
if (end == p) break;
band = Gow3View::ClampBand(b);
p = *end == ',' ? end + 1 : end;
}
return;
}
if (key == "upscaler") {
for (int u = 0; u < UpscalerCount; ++u) {
if (value == UpscalerName(u)) {
@@ -106,6 +123,18 @@ void Set(Values& v, const std::string& key, const std::string& value) {
v.debug_view = std::clamp(i, 0, DebugViewCount - 1);
} else if (key == "show_fps") {
v.show_fps = i != 0;
} else if (key == "mouse_enabled") {
v.mouse_enabled = i != 0;
} else if (key == "mouse_invert_y") {
v.mouse_invert_y = i != 0;
} else if (key == "mouse_sensitivity") {
v.mouse_sensitivity = Clamp(value.empty() ? 1.0f : f, 0.05f, 10.0f);
} else if (key == "mouse_smoothing") {
v.mouse_smoothing = Clamp(f, 0.0f, 0.95f);
} else if (key == "mouse_accel") {
v.mouse_accel = Clamp(f, 0.0f, 2.0f);
} else if (key == "mouse_deadzone") {
v.mouse_deadzone = Clamp(f, 0.0f, 0.5f);
} else if (key == "sensor_aim") {
v.sensor_aim = std::clamp(i, 25, 200);
} else if (key == "graffiti_guide_seen") {
@@ -152,6 +181,18 @@ void Set(Values& v, const std::string& key, const std::string& value) {
v.blue_orb_multiplier = Gow3Orbs::ClampMultiplier(f);
} else if (key == "gold_orb_multiplier") {
v.gold_orb_multiplier = Gow3Orbs::ClampMultiplier(f);
} else if (key == "lod_scale") {
v.lod_scale = Gow3View::ClampLodScale(f);
} else if (key == "region_lod") {
v.region_lod = Gow3View::ClampRegionLod(f);
} else if (key == "region_fade") {
v.region_fade = Gow3View::ClampRegionFade(f);
} else if (key == "near_clip") {
v.near_clip = Gow3View::ClampNearClip(f);
} else if (key == "far_clip") {
v.far_clip = Gow3View::ClampFarClip(f);
} else if (key == "mip_lod_bias") {
v.mip_lod_bias = Gow3View::ClampMipLodBias(f);
} else if (key == "fsr4_auto_exposure") {
v.fsr4_auto_exposure = i != 0;
} else if (key == "fsr4_invert_jitter") {
@@ -177,7 +218,7 @@ Values& Get() {
void Load() {
auto& v = Get();
if (FILE* file = std::fopen(Path(), "r")) {
char line[256];
char line[1024]; // key_map can be long
while (std::fgets(line, sizeof(line), file)) {
std::string text{line};
text.erase(text.find_last_not_of(" \t\r\n") + 1);
@@ -206,6 +247,13 @@ void Load() {
{"GOW3_STALE_READBACK", "stale_readback"}, {"GOW3_FSR1", "fsr1"},
{"GOW3_RCAS", "rcas"}, {"GOW3_FRAMES_AHEAD", "frames_queued"},
{"GOW3_SHOW_FPS", "show_fps"}, {"GOW3_SENSOR_AIM", "sensor_aim"},
{"GOW3_MOUSE", "mouse_enabled"}, {"GOW3_MOUSE_SENS", "mouse_sensitivity"},
{"GOW3_MOUSE_INVERT_Y", "mouse_invert_y"}, {"GOW3_MOUSE_SMOOTHING", "mouse_smoothing"},
{"GOW3_MOUSE_ACCEL", "mouse_accel"}, {"GOW3_MOUSE_DEADZONE", "mouse_deadzone"},
{"GOW3_LOD_SCALE", "lod_scale"}, {"GOW3_LOD_BANDS", "lod_bands"},
{"GOW3_REGION_LOD", "region_lod"}, {"GOW3_REGION_FADE", "region_fade"},
{"GOW3_NEAR_CLIP", "near_clip"}, {"GOW3_FAR_CLIP", "far_clip"},
{"GOW3_MIP_LOD_BIAS", "mip_lod_bias"},
};
for (const auto& [env, key] : env_keys) {
if (const char* value = std::getenv(env); value && value[0]) {
@@ -333,6 +381,12 @@ void Save() {
int(v.restart_unconfirmed.load()));
std::fprintf(file, "sensor_aim=%d\ngraffiti_guide_seen=%d\n", v.sensor_aim.load(),
int(v.graffiti_guide_seen.load()));
std::fprintf(file,
"lod_scale=%.3f\nregion_lod=%.3f\nregion_fade=%.3f\nnear_clip=%.3f\nfar_clip=%.0f\n"
"mip_lod_bias=%.3f\nlod_bands=%.0f,%.0f,%.0f,%.0f,%.0f,%.0f\n",
v.lod_scale.load(), v.region_lod.load(), v.region_fade.load(), v.near_clip.load(),
v.far_clip.load(), v.mip_lod_bias.load(), v.lod_bands[0].load(), v.lod_bands[1].load(),
v.lod_bands[2].load(), v.lod_bands[3].load(), v.lod_bands[4].load(), v.lod_bands[5].load());
// Fixes: applied on the next start by the launcher (GOW3_BPnn / GOW3_<NAME>); only the ones
// the player or the launcher chose are written.
for (int n = 0; n < kBackportPrefs; ++n) {
@@ -341,6 +395,11 @@ void Save() {
for (int e = 0; e < kExtraPrefs; ++e) {
if (const int p = v.extra_pref[e]; p >= 0) std::fprintf(file, "%s=%d\n", kExtraKeys[e], p);
}
std::fprintf(file,
"mouse_enabled=%d\nmouse_sensitivity=%.2f\nmouse_invert_y=%d\nmouse_smoothing=%.2f\n"
"mouse_accel=%.2f\nmouse_deadzone=%.2f\nkey_map=%s\n",
int(v.mouse_enabled.load()), v.mouse_sensitivity.load(), int(v.mouse_invert_y.load()),
v.mouse_smoothing.load(), v.mouse_accel.load(), v.mouse_deadzone.load(), v.key_map.c_str());
// Read by run.sh at start.
std::fprintf(file, "live_resolution=%s\n", v.live_resolution < 0 ? "auto"
: v.live_resolution ? "1" : "0");
+20 -3
View File
@@ -9,6 +9,7 @@
#include "../../src/cheat_hook.h"
#include <atomic>
#include <string>
#include <cstdint>
namespace Gow3Settings {
@@ -77,7 +78,7 @@ struct Values {
std::atomic<bool> async_shaders{true};
std::atomic<bool> fsr1{false};
std::atomic<bool> rcas{true};
std::atomic<int> rcas_strength{75};
std::atomic<int> rcas_strength{60};
/// Set by the presenter: sharpening ran on the last frame (it needs FSR 1 when the game's
/// image is smaller than the window).
std::atomic<bool> rcas_applied{false};
@@ -88,7 +89,7 @@ struct Values {
std::atomic<int> render_resolution{0};
std::atomic<int> engine_fps{120};
std::atomic<bool> deferred_readback{true};
std::atomic<bool> stale_readback{true};
std::atomic<bool> stale_readback{false};
/// Set before an "Apply and restart"; cleared once the new launch shows the game. run.py
/// restores the previous startup options when a launch never cleared it.
std::atomic<bool> restart_unconfirmed{false};
@@ -96,7 +97,7 @@ struct Values {
/// The engine rate this launch really runs at (run.py's GOW3_ENGINE_FPS): 60 when the frame
/// rate patch does not apply to this game build.
std::atomic<int> applied_engine_fps{120};
bool startup_deferred_readback = true, startup_stale_readback = true;
bool startup_deferred_readback = true, startup_stale_readback = false;
// FSR 4 checks (menu): the provider's auto exposure, the jitter sign it is given.
std::atomic<bool> fsr4_auto_exposure{true};
std::atomic<bool> fsr4_invert_jitter{false};
@@ -128,6 +129,22 @@ struct Values {
bool startup_object_motion = true;
int startup_output_res = OutputDefault;
int startup_live_resolution = 0;
// Draw distance (gow3_view.h, src/view_hook.h; inFAMOUS 1.07 only, view_supported). Live:
// the LOD values on the next frame, the clip planes for cameras the game creates afterwards.
std::atomic<float> lod_scale{1.0f}, region_lod{1.0f}, region_fade{1.0f};
std::atomic<float> near_clip{10.0f}, far_clip{1.0e7f};
std::atomic<float> lod_bands[6]{400.0f, 750.0f, 1500.0f, 3500.0f, 6000.0f, 15000.0f};
std::atomic<bool> view_supported{false};
/// Extra texture LOD bias of the game's draws (vk_rasterizer.cpp), any game.
std::atomic<float> mip_lod_bias{0.0f};
// Mouse and keyboard (src/runtime_pad.c through gow3gpu_input_settings, src/mouse_stick.h):
// the mouse turns the camera (right stick). Live from the menu.
std::atomic<bool> mouse_enabled{true}, mouse_invert_y{false};
std::atomic<float> mouse_sensitivity{1.0f}, mouse_smoothing{0.5f}, mouse_accel{0.0f}, mouse_deadzone{0.1f};
/// Keyboard and mouse button remap (GOW3_KEY_MAP syntax); written by Load only, read once.
std::string key_map;
};
Values& Get();
+13
View File
@@ -72,6 +72,19 @@ enum : std::uint64_t {
/// Guest threads wait for their own readback copies (GOW3_DEFERRED_READBACK=1 enables it;
/// this bit switches it off live for A/B, see BufferCache::ReadMemory).
DeferredReadback = 1ull << 58,
/// ResolveOverlap answers an integer read of the top rows of a cached image with that image
/// (gow3_pc #348, off unless GOW3_PARTIAL_HEIGHT_VIEWS=1; this bit switches it off live).
PartialHeightViews = 1ull << 59,
/// Readbacks that write guest memory through the backing view invalidate the textures over it
/// (gow3_pc #331, off unless GOW3_BACKING_INVALIDATE=1; this bit switches it off live).
BackingInvalidate = 1ull << 60,
/// Image uploads and frees wait only for readback copies that share bytes with the image, not
/// just a page (gow3_pc #386; only with stale readbacks; this bit switches it off live).
ExactReadbackWaits = 1ull << 61,
/// GetProgram takes the variant of the previous lookup without building the specialization
/// when its inputs are unchanged (i3-pc 0120115; only with GOW3_VARIANT_REUSE=1 or =check; this
/// bit switches it off live).
VariantReuse = 1ull << 62,
// Bits 20-29 are used as raw debug toggles by the camera/object motion and the upscaler.
};
inline bool Disabled(std::uint64_t bit) {
+73
View File
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// gow3: inFAMOUS draw distance and clip planes (src/view_hook.h). The probe verifies the eboot,
// installs the two code hooks and calls gow3_view_attach; a thread then keeps the game's data
// (LOD table, band distances, default clip planes) equal to the menu's values, so changes are
// live and survive the game rewriting them (debug LOD modes, script resets).
#include "gow3_settings.h"
#include "gow3_view.h"
#include "../../src/view_hook.h"
#include <chrono>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <thread>
namespace {
unsigned char* image = nullptr;
// Read by the hook stubs: {LOD_ADJUST weight, 1.0f} and 1 / FADE_DISTANCE scale.
alignas(16) float region_params[2] = {1.0f, 1.0f};
alignas(16) float fade_inverse = 1.0f;
void Put(uint32_t at, float value) {
float current;
std::memcpy(&current, image + at, 4);
if (current != value) {
std::memcpy(image + at, &value, 4);
}
}
void Apply() {
const auto& s = Gow3Settings::Get();
const float scale = Gow3View::ClampLodScale(s.lod_scale);
for (unsigned r = 0; r < GOW3_VIEW_LOD_ROWS; ++r) {
Put(GOW3_VIEW_LOD_TABLE + r * GOW3_VIEW_LOD_STRIDE, gow3_view_lod_original[r] * scale);
}
for (unsigned b = 0; b < GOW3_VIEW_BAND_COUNT; ++b) {
Put(GOW3_VIEW_BANDS + 4 * b, Gow3View::ClampBand(s.lod_bands[b]));
}
Put(GOW3_VIEW_NEAR_CLIP, Gow3View::ClampNearClip(s.near_clip));
Put(GOW3_VIEW_FAR_CLIP, Gow3View::ClampFarClip(s.far_clip));
region_params[0] = Gow3View::ClampRegionLod(s.region_lod);
fade_inverse = 1.0f / Gow3View::ClampRegionFade(s.region_fade);
}
} // namespace
extern "C" uintptr_t gow3_view_region_params(void) {
return reinterpret_cast<uintptr_t>(region_params);
}
extern "C" uintptr_t gow3_view_fade_inverse(void) {
return reinterpret_cast<uintptr_t>(&fade_inverse);
}
extern "C" void gow3_view_attach(unsigned char* guest_image) {
image = guest_image;
Apply(); // before the game's first frame
const auto& s = Gow3Settings::Get();
std::printf("Draw distance: LOD scale %.2f, bands %.0f/%.0f/%.0f/%.0f/%.0f/%.0f, region LOD_ADJUST weight %.2f, "
"FADE_DISTANCE x%.2f, near %.1f, far %.0f, texture LOD bias %+.2f\n",
s.lod_scale.load(), s.lod_bands[0].load(), s.lod_bands[1].load(), s.lod_bands[2].load(),
s.lod_bands[3].load(), s.lod_bands[4].load(), s.lod_bands[5].load(), s.region_lod.load(),
s.region_fade.load(), s.near_clip.load(), s.far_clip.load(), s.mip_lod_bias.load());
Gow3Settings::Get().view_supported = true;
std::thread([] {
for (;;) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
Apply();
}
}).detach();
}
+25
View File
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// gow3: ranges of the draw distance settings (gow3_view.cpp, src/view_hook.h).
#pragma once
#include <algorithm>
#include <cmath>
namespace Gow3View {
inline float Clamp(float value, float lo, float hi, float fallback) {
return std::isfinite(value) ? std::clamp(value, lo, hi) : fallback;
}
/// Camera LOD factor multiplier: every LOD band (and the game's dynamic quality steps) scale with it.
inline float ClampLodScale(float v) { return Clamp(v, 0.25f, 4.0f, 1.0f); }
/// How much the regions' script LOD_ADJUST counts: 1 the game's, 0 ignored.
inline float ClampRegionLod(float v) { return Clamp(v, 0.0f, 1.0f, 1.0f); }
/// FADE_DISTANCE multiplier: the distance over which a region's settings blend in.
inline float ClampRegionFade(float v) { return Clamp(v, 0.25f, 4.0f, 1.0f); }
/// Default near plane of new cameras (game units, the game uses 10).
inline float ClampNearClip(float v) { return Clamp(v, 1.0f, 100.0f, 10.0f); }
/// Default far plane of new cameras (the game uses 1e7, effectively unlimited).
inline float ClampFarClip(float v) { return Clamp(v, 1000.0f, 1.0e7f, 1.0e7f); }
/// One LOD band distance (game units); the game's are 400, 750, 1500, 3500, 6000 and 15000.
inline float ClampBand(float v) { return Clamp(v, 1.0f, 1.0e6f, 400.0f); }
/// Extra texture LOD bias of the game's draws: negative is sharper, positive is blurrier.
inline float ClampMipLodBias(float v) { return Clamp(v, -3.0f, 3.0f, 0.0f); }
} // namespace Gow3View
+1 -1
View File
@@ -14,7 +14,7 @@
<TitleID>
<ID>CUSA00004</ID>
</TitleID>
<Metadata Title="inFAMOUS Second Son" Name="Dynamic quality thresholds x2 (no quality drops in busy scenes)" Note="The dynamic quality system of the game lowers the quality (sun shadow cascades, resolution) when the per-frame GPU command buffer gets busy; its limits are doubled. FR : limites du systeme de qualite dynamique du jeu doublees : il ne baisse plus la qualite (ombres du soleil, resolution) dans les scenes chargees." Author="AYOUB1080p" PatchVer="1.0" AppVer="01.00" AppElf="eboot.bin" isEnabled="true">
<Metadata Title="inFAMOUS Second Son" Name="Dynamic quality thresholds x2 (no quality drops in busy scenes)" Note="The dynamic quality system of the game lowers the quality (sun shadow cascades, resolution) when the per-frame GPU command buffer gets busy; its limits are doubled. FR : limites du systeme de qualite dynamique du jeu doublees : il ne baisse plus la qualite (ombres du soleil, resolution) dans les scenes chargees." Author="AYOUB1080p" PatchVer="1.0" AppVer="01.00" AppElf="eboot.bin" isEnabled="false">
<PatchList>
<Line Type="bytes" Address="0x005f1736" Value="00000003" OriginalHash="4b72757f9c5c7dc2"/>
<Line Type="bytes" Address="0x005f1b19" Value="CCCC8C02" OriginalHash="2c21d479393d51d0"/>
+7 -2
View File
@@ -15,12 +15,17 @@ PROFILES = {
title: {
# Precise GPU readbacks: the game reads GPU particle results back (the smoke to drain,
# drones); without them those effects are missing. The deferred and eager copies keep
# the waits short (HayatoG's iss_pc defaults for this game).
# the waits short (HayatoG's iss_pc defaults; his eager read-ahead made smoke flicker).
'GOW3_READBACKS': '2',
# Buffer-store barriers in uniform loops of workgroups wider than a GCN wave (AYOUB1080p).
'GOW3_WAVE_MEMORY_BARRIERS': '1',
'GOW3_DEFERRED_READBACK': '1',
'GOW3_EAGER_READBACK': '1',
# Draw distance (HayatoG's recommended setup, 1.07 only: src/view_hook.h): a shorter LOD
# distance means far fewer draws in the open city, the bottleneck here.
'GOW3_LOD_SCALE': '0.6',
'GOW3_REGION_FADE': '0.8',
# AMD: maximal reconvergence is suspected of keeping the game from starting (RX 7600).
'GOW3_BP06': '0',
# GOW3_FULL_BARRIERS=1 is no longer needed: the black 64x64 tiles stay away without it
# since the vertex/index/indirect buffer barriers (BP10, HayatoG's core); off, the FPS
# roughly doubles in the city (tester, RX 9060 XT).
+41
View File
@@ -12,6 +12,7 @@
#include "cheat_hook.h"
#include "actor_hook.h"
#include "damage_hook.h"
#include "view_hook.h"
#include "gpu/gow3gpu.h"
#if !defined(__x86_64__) || !defined(__GNUC__)
#error This prototype requires x86-64 GCC or Clang (including MinGW).
@@ -617,6 +618,45 @@ static int install_jump(uint64_t at, unsigned length, unsigned char *stub) {
return 1;
}
extern uintptr_t gow3_view_region_params(void);
extern uintptr_t gow3_view_fade_inverse(void);
extern void gow3_view_attach(unsigned char *guest_image);
/* issn: draw distance, LOD and clip planes of inFAMOUS 1.07 (src/view_hook.h, from HayatoG's
* iss_pc): the menu's/environment's values reach the game's data through gow3_view_attach; the
* regions' LOD_ADJUST and FADE_DISTANCE go through four stubs. Silent for other builds. */
static void install_view_tuning(Segment *segments, uint64_t count, uint64_t size) {
if (!mapped(segments, count, GOW3_VIEW_LOD_TABLE, GOW3_VIEW_LOD_ROWS * GOW3_VIEW_LOD_STRIDE) ||
!mapped(segments, count, GOW3_VIEW_BANDS, 4 * GOW3_VIEW_BAND_COUNT) ||
!mapped(segments, count, GOW3_VIEW_NEAR_CLIP, 8) ||
!mapped(segments, count, gow3_view_region_sites[1], sizeof(gow3_view_region_code)) ||
!gow3_view_signature_match(image, size)) {
return;
}
unsigned char *stubs = allocate(page_size);
const uint32_t *sites[2] = {gow3_view_region_sites, gow3_view_fade_sites};
for (unsigned kind = 0; kind < 2; ++kind) {
for (unsigned i = 0; i < 2; ++i) {
const uint32_t at = sites[kind][i];
unsigned char *stub = stubs + (kind * 2 + i) * 128;
const int64_t delta = (int64_t)(uintptr_t)stub - (int64_t)(uintptr_t)(image + at + 5);
if (delta < INT32_MIN || delta > INT32_MAX) {
printf("Draw distance: unavailable (hook outside relative-jump range)\n");
return;
}
const uintptr_t resume = (uintptr_t)(image + at + GOW3_VIEW_SITE_LENGTH);
if (kind == 0) gow3_view_build_region_stub(stub, gow3_view_region_params(), resume);
else gow3_view_build_fade_stub(stub, gow3_view_fade_inverse(), resume);
}
}
protect(stubs, page_size, 5);
for (unsigned kind = 0; kind < 2; ++kind)
for (unsigned i = 0; i < 2; ++i)
install_jump(sites[kind][i], GOW3_VIEW_SITE_LENGTH, stubs + (kind * 2 + i) * 128);
gow3_view_attach(image);
printf("Draw distance: ready (LOD scale, LOD bands, region LOD_ADJUST and FADE_DISTANCE, near/far clip)\n");
}
/* Damage dealt/taken and green/blue/gold orb multipliers (src/damage_hook.h, src/orb_hook.h):
* the stubs read the menu's floats directly, so changes apply on the next hit or pickup. */
static void install_stat_multipliers(Segment *segments, uint64_t count, uint64_t size) {
@@ -1045,6 +1085,7 @@ int main(int argc, char **argv) {
if (gpu_enabled) install_cheats(segments, ns, size);
if (gpu_enabled) install_actor_watch(segments, ns, size);
if (gpu_enabled) install_stat_multipliers(segments, ns, size);
if (gpu_enabled) install_view_tuning(segments, ns, size);
#ifdef _WIN32
printf("Guest TCB loads redirected to the TEB TLS slot: %" PRIu64 "\n", patch_tcb_loads(segments, ns));
#endif
+110
View File
@@ -0,0 +1,110 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/* inFAMOUS Second Son (CUSA00223 01.07 eboot): draw distance, LOD and clip planes.
* Offsets are eboot virtual addresses (= image offsets). Found with Ghidra (PORT_INFAMOUS.md, 16/10):
* - camera LOD factor (camera+0x5a8) = FOV term * region LOD_ADJUST * kViewLodTable[level].lod;
* every LOD band distance (kViewBands) is multiplied by it. `level` (0..20) is the game's
* dynamic quality step, raised when the frame's draw lists get too big.
* - region settings (script NEAR_CLIP / LOD_ADJUST / ...) blend into a block whose LOD_ADJUST is
* read by the two kViewRegionSites (vmulss 0x9c(%rax)); FADE_DISTANCE (region+0x2f0) is the
* distance over which a region's settings fade in (kViewFadeSites, vdivss 0x2f0(%rbx)).
* - new cameras copy the near (10) and far (1e7) clip planes from two globals.
* Data writes need no hook; the two code sites jump to stubs that read the menu's floats. */
#ifndef GOW3_VIEW_HOOK_H
#define GOW3_VIEW_HOOK_H
#include <stdint.h>
#include <string.h>
#define GOW3_VIEW_LOD_TABLE 0xda08c0u /* 21 rows of 0x14 bytes: float lod, int shadow cascades
delta, float max distance, float, int */
#define GOW3_VIEW_LOD_ROWS 21u
#define GOW3_VIEW_LOD_STRIDE 0x14u
#define GOW3_VIEW_BANDS 0xdfaa34u /* 6 floats: LOD band distances (a 7th, FLT_MAX, follows) */
#define GOW3_VIEW_BAND_COUNT 6u
#define GOW3_VIEW_NEAR_CLIP 0xdf9e64u /* float 10.0: new cameras' near plane, the regions' default */
#define GOW3_VIEW_FAR_CLIP 0xdf9e68u /* float 1e7: new cameras' far plane */
#define GOW3_VIEW_SITE_LENGTH 8u
static const float gow3_view_lod_original[GOW3_VIEW_LOD_ROWS] = {
1.0f, 0.9f, 0.8f, 0.7f, 0.6f, 0.5f, 0.5f, 0.4f, 0.4f, 0.3f, 0.2f,
0.2f, 0.2f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f};
static const float gow3_view_band_original[GOW3_VIEW_BAND_COUNT] = {400.0f, 750.0f, 1500.0f,
3500.0f, 6000.0f, 15000.0f};
#define GOW3_VIEW_NEAR_ORIGINAL 10.0f
#define GOW3_VIEW_FAR_ORIGINAL 1.0e7f
/* vmulss 0x9c(%rax),%xmm0,%xmm0, then movslq 0x1dca8(%rcx),%rax: rax is dead after the multiply. */
static const uint32_t gow3_view_region_sites[2] = {0x581088u, 0x5811ffu};
static const unsigned char gow3_view_region_code[15] = {0xc5, 0xfa, 0x59, 0x80, 0x9c, 0, 0, 0,
0x48, 0x63, 0x81, 0xa8, 0xdc, 0x01, 0x00};
/* vdivss 0x2f0(%rbx),%xmm0,%xmm0 (the region's FADE_DISTANCE) */
static const uint32_t gow3_view_fade_sites[2] = {0x1bae75u, 0x1bb1e9u};
static const unsigned char gow3_view_fade_code[8] = {0xc5, 0xfa, 0x5e, 0x83, 0xf0, 0x02, 0, 0};
static inline int gow3_view_float_is(const unsigned char *image, uint32_t at, float value) {
float v;
memcpy(&v, image + at, 4);
return v == value;
}
/* The code and the data the hooks rely on, all unchanged (this eboot build). */
static inline int gow3_view_signature_match(const unsigned char *image, uint64_t size) {
if (size < GOW3_VIEW_BANDS + 4 * GOW3_VIEW_BAND_COUNT) return 0;
for (unsigned i = 0; i < 2; ++i) {
if (memcmp(image + gow3_view_region_sites[i], gow3_view_region_code, sizeof(gow3_view_region_code)) ||
memcmp(image + gow3_view_fade_sites[i], gow3_view_fade_code, sizeof(gow3_view_fade_code))) return 0;
}
for (unsigned r = 0; r < GOW3_VIEW_LOD_ROWS; ++r)
if (!gow3_view_float_is(image, GOW3_VIEW_LOD_TABLE + r * GOW3_VIEW_LOD_STRIDE, gow3_view_lod_original[r])) return 0;
for (unsigned b = 0; b < GOW3_VIEW_BAND_COUNT; ++b)
if (!gow3_view_float_is(image, GOW3_VIEW_BANDS + 4 * b, gow3_view_band_original[b])) return 0;
return gow3_view_float_is(image, GOW3_VIEW_NEAR_CLIP, GOW3_VIEW_NEAR_ORIGINAL) &&
gow3_view_float_is(image, GOW3_VIEW_FAR_CLIP, GOW3_VIEW_FAR_ORIGINAL);
}
static inline unsigned gow3_view_jump_back(unsigned char *out, uintptr_t resume) {
const unsigned char jmp[6] = {0xff, 0x25, 0, 0, 0, 0}; /* jmp *0(%rip) */
memcpy(out, jmp, 6);
memcpy(out + 6, &resume, 8);
return 14;
}
/* LOD_ADJUST weight: xmm0 *= 1 + (adjust - 1) * weight; params = {weight, 1.0f}. ymm1 is saved
* below the red zone, rax is free (the next instruction loads it). */
static inline unsigned gow3_view_build_region_stub(unsigned char *out, uintptr_t params, uintptr_t resume) {
static const unsigned char head[] = {
0x48, 0x8d, 0xa4, 0x24, 0x60, 0xff, 0xff, 0xff, /* lea -0xa0(%rsp),%rsp */
0xc5, 0xfc, 0x11, 0x0c, 0x24, /* vmovups %ymm1,(%rsp) */
0xc5, 0xfa, 0x10, 0x88, 0x9c, 0, 0, 0, /* vmovss 0x9c(%rax),%xmm1 */
0x48, 0xb8}; /* movabs $params,%rax */
static const unsigned char tail[] = {
0xc5, 0xf2, 0x5c, 0x48, 0x04, /* vsubss 0x4(%rax),%xmm1,%xmm1 */
0xc5, 0xf2, 0x59, 0x08, /* vmulss (%rax),%xmm1,%xmm1 */
0xc5, 0xf2, 0x58, 0x48, 0x04, /* vaddss 0x4(%rax),%xmm1,%xmm1 */
0xc5, 0xfa, 0x59, 0xc1, /* vmulss %xmm1,%xmm0,%xmm0 */
0xc5, 0xfc, 0x10, 0x0c, 0x24, /* vmovups (%rsp),%ymm1 */
0x48, 0x8d, 0xa4, 0x24, 0xa0, 0, 0, 0}; /* lea 0xa0(%rsp),%rsp */
unsigned n = 0;
memcpy(out + n, head, sizeof(head)); n += sizeof(head);
memcpy(out + n, &params, 8); n += 8;
memcpy(out + n, tail, sizeof(tail)); n += sizeof(tail);
return n + gow3_view_jump_back(out + n, resume);
}
/* FADE_DISTANCE scale: the game's division, then xmm0 *= *inverse_scale (rax saved). */
static inline unsigned gow3_view_build_fade_stub(unsigned char *out, uintptr_t inverse_scale, uintptr_t resume) {
static const unsigned char head[] = {
0xc5, 0xfa, 0x5e, 0x83, 0xf0, 0x02, 0, 0, /* vdivss 0x2f0(%rbx),%xmm0,%xmm0 */
0x48, 0x8d, 0x64, 0x24, 0x80, /* lea -0x80(%rsp),%rsp */
0x50, /* push %rax */
0x48, 0xb8}; /* movabs $inverse_scale,%rax */
static const unsigned char tail[] = {
0xc5, 0xfa, 0x59, 0x00, /* vmulss (%rax),%xmm0,%xmm0 */
0x58, /* pop %rax */
0x48, 0x8d, 0xa4, 0x24, 0x80, 0, 0, 0}; /* lea 0x80(%rsp),%rsp */
unsigned n = 0;
memcpy(out + n, head, sizeof(head)); n += sizeof(head);
memcpy(out + n, &inverse_scale, 8); n += 8;
memcpy(out + n, tail, sizeof(tail)); n += sizeof(tail);
return n + gow3_view_jump_back(out + n, resume);
}
#endif