mirror of
https://github.com/Liondarkz/issn_pc.git
synced 2026-11-07 12:47:50 -10:00
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:
28 files changed
+1386
-71
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(¤t, 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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
@@ -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
@@ -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, ¶ms, 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
|
||||
Reference in new issue
Block a user