mirror of
https://github.com/SpeedBreakerProject/speedbreaker.git
synced 2026-10-23 08:02:35 +02:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
241 lines
8.7 KiB
C++
241 lines
8.7 KiB
C++
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
|
|
//
|
|
// Occlusion query reports (EVENT_WRITE_ZPD), as this game's D3D reads them:
|
|
// the bookkeeping of gpu/renderer.cpp's counting (NFSMW_OCCLUSION=1) and the
|
|
// store order of an END report. Vulkan-free: tests/zpd_report_test.cpp
|
|
// checks it alone.
|
|
//
|
|
// D3D's Issue writes each query's reports into a 64-byte block per EDRAM
|
|
// tile: the BEGIN ZPD at block+0x20, the END ZPD at block+0x00 (the latched
|
|
// RB_SAMPLE_COUNT_ADDR). A report is 8 dwords, Total, ZFail, ZPass and
|
|
// StencilFail, each an A and a B lane, read little-endian (lwbrx). Issue
|
|
// BEGIN stores 0xFFFFFEED (big-endian) in the A lanes (+0/+8/+16/+24) of the
|
|
// LAST tile's END report; GetData is pending while all four still hold it,
|
|
// then returns the sum over tiles of END.ZPass(A+B) - BEGIN.ZPass(A+B). The
|
|
// game takes a count below 2^24 only. Every BEGIN report is all zeros, so an
|
|
// END report holds the samples of its own bracket: valid whenever it lands,
|
|
// even after the next BEGIN (a running counter would then go negative).
|
|
#pragma once
|
|
#include <array>
|
|
#include <atomic>
|
|
#include <cstdint>
|
|
|
|
namespace gpu::zpd
|
|
{
|
|
enum class Kind { Begin, End, Unknown };
|
|
|
|
// By the report's offset in its block.
|
|
inline Kind Classify(uint32_t address)
|
|
{
|
|
switch (address & 0x3F)
|
|
{
|
|
case 0x20: return Kind::Begin;
|
|
case 0x00: return Kind::End;
|
|
default: return Kind::Unknown;
|
|
}
|
|
}
|
|
|
|
// Guest samples for `raw` host samples: `num` guest samples per host
|
|
// pixel (the guest surface's MSAA; the host renders 1 sample), over
|
|
// `den` host pixels per guest pixel (the render scale, sx * sy). Rounded
|
|
// to nearest, at least 1 when anything passed (as Xenia does).
|
|
inline uint64_t Normalize(uint64_t raw, uint32_t num, uint32_t den)
|
|
{
|
|
if (raw == 0)
|
|
return 0;
|
|
if (den == 0)
|
|
den = 1;
|
|
uint64_t guest = (raw * num + den / 2) / den;
|
|
return guest ? guest : 1;
|
|
}
|
|
|
|
// An END's count from its segments' sum. A runaway one is rejected by
|
|
// the game, which keeps its value (as from the console), so it is
|
|
// saturated at 2^24, never cut to 32 bits: GetData keeps the low 32 bits
|
|
// of its sum over tiles, which no sum of up to 255 saturated tiles wraps
|
|
// into a count the game would take.
|
|
constexpr uint64_t kRunaway = 1u << 24;
|
|
inline uint32_t EndCount(uint64_t sum)
|
|
{
|
|
return uint32_t(sum < kRunaway ? sum : kRunaway);
|
|
}
|
|
|
|
// An END report of `count` samples, in host order (the guest reads it
|
|
// byte-swapped: no swap here). Total_A = ZPass_A = count, the rest 0.
|
|
inline std::array<uint32_t, 8> ComposeEnd(uint32_t count)
|
|
{
|
|
return { count, 0, 0, 0, count, 0, 0, 0 };
|
|
}
|
|
|
|
// The order of its stores: the B lanes (no marker there), then ZPass_A,
|
|
// which makes the report ready with its count in place, then the other
|
|
// A lanes; a release fence between the three. GetData's loads are
|
|
// unordered (no barrier in it), so a read racing them sees the marker in
|
|
// all four A lanes (pending), the marker in a lane it sums (0xEDFEFFFF
|
|
// read little-endian: >= 2^24, which the game rejects) or real counts:
|
|
// every B lane and BEGIN report is 0, so each tile adds its ZPass_A, this
|
|
// END's or (a stale load) the one before. Untiled only tile 0 counts:
|
|
// this END's count or the last. With NFSMW_TILING=1 one sum can mix
|
|
// tiles of adjacent frames (also when a late END lands after the next
|
|
// BEGIN's marker and makes that query ready). Indices into the 8
|
|
// dwords; phases of 4, 1 and 3.
|
|
constexpr uint8_t kEndOrder[8] = { 1, 3, 5, 7, 4, 0, 2, 6 };
|
|
constexpr uint8_t kEndPhaseEnd[3] = { 4, 5, 8 };
|
|
|
|
// `store(offset, value)` stores one dword of the report (offset in bytes;
|
|
// the callers open the write watch's guard once around all eight).
|
|
template<typename Store>
|
|
void StoreEnd(uint32_t count, Store&& store)
|
|
{
|
|
const std::array<uint32_t, 8> v = ComposeEnd(count);
|
|
uint32_t i = 0;
|
|
for (uint32_t phase = 0; phase < 3; phase++)
|
|
{
|
|
if (phase)
|
|
std::atomic_thread_fence(std::memory_order_release);
|
|
for (; i < kEndPhaseEnd[phase]; i++)
|
|
store(uint32_t(kEndOrder[i]) * 4, v[kEndOrder[i]]);
|
|
}
|
|
}
|
|
|
|
// A query bracket: BEGIN seen, END not yet. `s0` is the first counting
|
|
// segment that can belong to it (segments are numbered in recording
|
|
// order and never span a ZPD), `frame` when it was opened.
|
|
struct Bracket
|
|
{
|
|
uint32_t beginAddress = 0;
|
|
uint64_t s0 = 0, frame = 0;
|
|
};
|
|
|
|
// The open brackets, oldest first; the command processor's.
|
|
class BracketBook
|
|
{
|
|
public:
|
|
static constexpr uint32_t kMax = 8;
|
|
enum class Opened { New, Replaced, DroppedOldest };
|
|
|
|
// A BEGIN at `beginAddress`. One already open there (a query issued
|
|
// again before its END) is replaced; beyond kMax the oldest is
|
|
// dropped. `old`: the replaced or dropped bracket.
|
|
Opened Open(uint32_t beginAddress, uint64_t s0, uint64_t frame, Bracket* old = nullptr)
|
|
{
|
|
Opened result = Opened::New;
|
|
for (uint32_t i = 0; i < m_count; i++)
|
|
if (m_open[i].beginAddress == beginAddress)
|
|
{
|
|
if (old)
|
|
*old = m_open[i];
|
|
Erase(i);
|
|
result = Opened::Replaced;
|
|
break;
|
|
}
|
|
if (result == Opened::New && m_count == kMax)
|
|
{
|
|
if (old)
|
|
*old = m_open[0];
|
|
Erase(0);
|
|
result = Opened::DroppedOldest;
|
|
}
|
|
m_open[m_count++] = { beginAddress, s0, frame };
|
|
return result;
|
|
}
|
|
|
|
// The END at `endAddress` closes the bracket its BEGIN opened 0x20
|
|
// above it. False when none is open (its count is then 0).
|
|
bool Close(uint32_t endAddress, Bracket* closed = nullptr)
|
|
{
|
|
for (uint32_t i = 0; i < m_count; i++)
|
|
if (m_open[i].beginAddress == endAddress + 0x20)
|
|
{
|
|
if (closed)
|
|
*closed = m_open[i];
|
|
Erase(i);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Drops the brackets opened before `frame`: how many; `first`, the
|
|
// oldest of them.
|
|
uint32_t DropBefore(uint64_t frame, Bracket* first = nullptr)
|
|
{
|
|
uint32_t dropped = 0;
|
|
for (uint32_t i = 0; i < m_count;)
|
|
{
|
|
if (m_open[i].frame >= frame)
|
|
{
|
|
i++;
|
|
continue;
|
|
}
|
|
if (first && dropped == 0)
|
|
*first = m_open[i];
|
|
dropped++;
|
|
Erase(i);
|
|
}
|
|
return dropped;
|
|
}
|
|
|
|
bool Empty() const { return m_count == 0; }
|
|
uint32_t Size() const { return m_count; }
|
|
const Bracket& operator[](uint32_t i) const { return m_open[i]; }
|
|
|
|
private:
|
|
void Erase(uint32_t i)
|
|
{
|
|
for (; i + 1 < m_count; i++)
|
|
m_open[i] = m_open[i + 1];
|
|
m_count--;
|
|
}
|
|
|
|
Bracket m_open[kMax];
|
|
uint32_t m_count = 0;
|
|
};
|
|
|
|
// Finished segments' counts by segment id, the last kSize of them (the
|
|
// completion thread's). Each entry carries its id: a bracket summing an
|
|
// id that was never stored, or was overwritten, counts it as lost.
|
|
// Ids start at 1 (an empty entry's id is 0).
|
|
class ResultRing
|
|
{
|
|
public:
|
|
static constexpr uint32_t kSize = 4096;
|
|
|
|
void Put(uint64_t id, uint64_t guest, uint64_t raw)
|
|
{
|
|
m_entries[id % kSize] = { id, guest, raw };
|
|
}
|
|
|
|
// Guest samples of segments [s0, s1); `lost` and `raw` (host
|
|
// samples) are added to.
|
|
uint64_t Sum(uint64_t s0, uint64_t s1, uint32_t& lost, uint64_t* raw = nullptr) const
|
|
{
|
|
uint64_t sum = 0;
|
|
if (s1 > s0 && s1 - s0 > kSize)
|
|
{
|
|
lost += uint32_t(s1 - s0 - kSize);
|
|
s0 = s1 - kSize;
|
|
}
|
|
for (uint64_t id = s0; id < s1; id++)
|
|
{
|
|
const Entry& e = m_entries[id % kSize];
|
|
if (e.id != id || id == 0)
|
|
{
|
|
lost++;
|
|
continue;
|
|
}
|
|
sum += e.guest;
|
|
if (raw)
|
|
*raw += e.raw;
|
|
}
|
|
return sum;
|
|
}
|
|
|
|
private:
|
|
struct Entry
|
|
{
|
|
uint64_t id = 0, guest = 0, raw = 0;
|
|
};
|
|
Entry m_entries[kSize];
|
|
};
|
|
}
|