mirror of
https://github.com/jessicanataliagta/PSPRecomp
synced 2026-09-26 08:41:08 -04:00
5041 lines
244 KiB
C++
5041 lines
244 KiB
C++
#include "ge_renderer.hpp"
|
|
#include "ge_gpu_backend.hpp"
|
|
#include "vcs_config.hpp"
|
|
#include "vcs_project2dfx.hpp"
|
|
#include "vcs_fps_overlay.hpp"
|
|
|
|
#include "psprecomp/common.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <bit>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <chrono>
|
|
#include <condition_variable>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <functional>
|
|
#include <type_traits>
|
|
#include <limits>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
#if defined(_M_X64) || defined(_M_IX86) || defined(__x86_64__) || defined(__i386__)
|
|
#include <immintrin.h>
|
|
#define PSPRECOMP_GE_X86_SIMD 1
|
|
#else
|
|
#define PSPRECOMP_GE_X86_SIMD 0
|
|
#endif
|
|
|
|
namespace vcs {
|
|
|
|
namespace {
|
|
|
|
// Scalar helpers that the C library would otherwise turn into calls.
|
|
//
|
|
// A bilinear-filtered fragment ran about thirty function calls: sixteen
|
|
// std::lround (which MSVC resolves to __imp_lroundf, an *indirect* call through
|
|
// the CRT import table), roughly ten std::floor, and several std::isfinite --
|
|
// each of which is a single x86-64 instruction. Measured at ~400 cycles per
|
|
// fragment, that call traffic was the frame budget. These replacements compute
|
|
// exactly the same values inline.
|
|
|
|
// Same predicate as std::isfinite: exponent field all ones means inf or NaN.
|
|
[[nodiscard]] inline bool finite_float(float value) noexcept {
|
|
return (std::bit_cast<std::uint32_t>(value) & 0x7F800000u) != 0x7F800000u;
|
|
}
|
|
|
|
// Same value as static_cast<std::int32_t>(std::floor(value)), for inputs that
|
|
// fit in std::int32_t. Anything outside that range was already undefined at
|
|
// the cast, so the guard only has to keep this function itself well defined.
|
|
// Kept branchless on purpose. A first attempt used `? :` on the correction and
|
|
// measured *slower* than the CRT call it replaced: the comparison is
|
|
// data-dependent and mispredicts, while lroundf/floorf are branchless SSE.
|
|
[[nodiscard]] inline std::int32_t floor_to_int(float value) noexcept {
|
|
if (!(value > -2147483000.0f && value < 2147483000.0f))
|
|
return static_cast<std::int32_t>(std::floor(value));
|
|
const std::int32_t truncated = static_cast<std::int32_t>(value); // toward zero
|
|
return truncated - static_cast<std::int32_t>(static_cast<float>(truncated) > value);
|
|
}
|
|
|
|
// Same value as std::floor, for inputs inside the 32-bit integer range.
|
|
[[nodiscard]] inline float floor_float(float value) noexcept {
|
|
if (!(value > -2147483000.0f && value < 2147483000.0f)) return std::floor(value);
|
|
const float truncated = static_cast<float>(static_cast<std::int32_t>(value));
|
|
return truncated - static_cast<float>(truncated > value);
|
|
}
|
|
|
|
// Same value as static_cast<std::uint8_t>(std::clamp(std::lround(value), 0, 255)).
|
|
//
|
|
// std::lround rounds to nearest with ties away from zero. Inside (-0.5, 255.5)
|
|
// that is computed here from the exact fractional part -- `value - trunc(value)`
|
|
// is exact for |value| < 2^24 -- and outside it the clamp alone already decides
|
|
// the answer, so the rare path defers to std::lround and stays identical
|
|
// (NaN included, since both comparisons are false for it).
|
|
[[nodiscard]] inline std::uint8_t round_clamp_to_byte(float value) noexcept {
|
|
if (value > -0.5f && value < 255.5f) {
|
|
const long truncated = static_cast<long>(value);
|
|
const float fraction = value - static_cast<float>(truncated);
|
|
const long rounded = truncated + static_cast<long>(fraction >= 0.5f) -
|
|
static_cast<long>(fraction <= -0.5f);
|
|
return static_cast<std::uint8_t>(rounded < 0L ? 0L : (rounded > 255L ? 255L : rounded));
|
|
}
|
|
return static_cast<std::uint8_t>(std::clamp(std::lround(value), 0L, 255L));
|
|
}
|
|
|
|
// Packed IEEE divisions preserve the result of each scalar division while
|
|
// issuing one vector divide for two, three or four independent channels.
|
|
// Numerators are still formed in the original scalar order, so framebuffer
|
|
// results remain bit-identical to the old path.
|
|
inline void divide2_same_denominator(float n0, float n1, float denominator,
|
|
float &out0, float &out1) noexcept {
|
|
#if PSPRECOMP_GE_X86_SIMD
|
|
alignas(16) float values[4]{n0, n1, 0.0f, 0.0f};
|
|
const __m128 result = _mm_div_ps(_mm_load_ps(values), _mm_set1_ps(denominator));
|
|
_mm_store_ps(values, result);
|
|
out0 = values[0];
|
|
out1 = values[1];
|
|
#else
|
|
out0 = n0 / denominator;
|
|
out1 = n1 / denominator;
|
|
#endif
|
|
}
|
|
|
|
inline void divide3_same_denominator(float n0, float n1, float n2, float denominator,
|
|
float &out0, float &out1, float &out2) noexcept {
|
|
#if PSPRECOMP_GE_X86_SIMD
|
|
alignas(16) float values[4]{n0, n1, n2, 0.0f};
|
|
const __m128 result = _mm_div_ps(_mm_load_ps(values), _mm_set1_ps(denominator));
|
|
_mm_store_ps(values, result);
|
|
out0 = values[0];
|
|
out1 = values[1];
|
|
out2 = values[2];
|
|
#else
|
|
out0 = n0 / denominator;
|
|
out1 = n1 / denominator;
|
|
out2 = n2 / denominator;
|
|
#endif
|
|
}
|
|
|
|
inline void divide4_same_denominator(float n0, float n1, float n2, float n3,
|
|
float denominator, float (&out)[4]) noexcept {
|
|
#if PSPRECOMP_GE_X86_SIMD
|
|
alignas(16) float values[4]{n0, n1, n2, n3};
|
|
const __m128 result = _mm_div_ps(_mm_load_ps(values), _mm_set1_ps(denominator));
|
|
_mm_store_ps(values, result);
|
|
out[0] = values[0]; out[1] = values[1]; out[2] = values[2]; out[3] = values[3];
|
|
#else
|
|
out[0] = n0 / denominator; out[1] = n1 / denominator;
|
|
out[2] = n2 / denominator; out[3] = n3 / denominator;
|
|
#endif
|
|
}
|
|
|
|
// Phase split for the frame budget, under PSPRECOMP_GE_PHASE_DIAG.
|
|
//
|
|
// ge_us alone cannot say whether a heavy frame is spent per triangle (clipping,
|
|
// viewport transform, culling, bounding box) or per fragment (the pixel loop).
|
|
// Guessing that wrong costs a full measurement cycle, so both are timed
|
|
// directly. The GE list runs on one thread, so plain counters suffice; the
|
|
// clock reads are only taken when the diagnostic is on.
|
|
bool ge_phase_diag_enabled() noexcept {
|
|
static const bool enabled = std::getenv("PSPRECOMP_GE_PHASE_DIAG") != nullptr;
|
|
return enabled;
|
|
}
|
|
std::uint64_t g_ge_pixel_ns{};
|
|
std::uint64_t g_ge_triangle_count{};
|
|
// Stage 35 sub-phase accumulators. Written only while the diagnostic is on and
|
|
// only at draw-call granularity; see GePhaseTotals for why.
|
|
std::uint64_t g_ge_draw_setup_ns{};
|
|
std::uint64_t g_ge_texture_upload_ns{};
|
|
std::uint64_t g_ge_vertex_decode_ns{};
|
|
std::uint64_t g_ge_gpu_stage_ns{};
|
|
std::uint64_t g_ge_triangle_prep_ns{};
|
|
std::uint64_t g_ge_gpu_accumulate_ns{};
|
|
std::uint64_t g_ge_primitive_count{};
|
|
std::uint64_t g_ge_vertex_count{};
|
|
|
|
// Accumulates into `sink` only when the phase diagnostic is enabled, so the
|
|
// production path pays one predictable branch and no clock read.
|
|
struct PhaseTimer {
|
|
std::uint64_t *sink;
|
|
std::chrono::steady_clock::time_point entry;
|
|
explicit PhaseTimer(std::uint64_t &target) noexcept
|
|
: sink(ge_phase_diag_enabled() ? &target : nullptr),
|
|
entry(sink != nullptr ? std::chrono::steady_clock::now()
|
|
: std::chrono::steady_clock::time_point{}) {}
|
|
~PhaseTimer() {
|
|
if (sink == nullptr) return;
|
|
*sink += static_cast<std::uint64_t>(
|
|
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
|
std::chrono::steady_clock::now() - entry).count());
|
|
}
|
|
PhaseTimer(const PhaseTimer &) = delete;
|
|
PhaseTimer &operator=(const PhaseTimer &) = delete;
|
|
};
|
|
|
|
// The Stage 22 vertex staging path copies every triangle-stream vertex into the
|
|
// mapped upload ring, but nothing ever reads those bytes back: the frame that
|
|
// Vulkan actually draws is assembled from the prepared screen triangles in
|
|
// ge_gpu_backend, and finish_color_frame overwrites the same region with them.
|
|
// It was a bring-up proof that geometry could cross the host->Vulkan boundary,
|
|
// and it stayed on the per-draw path. Off by default now; set the variable to
|
|
// 1 to restore it for an A/B or for the transfer self-test.
|
|
bool legacy_vertex_staging_enabled() noexcept {
|
|
static const bool enabled = [] {
|
|
const char *text = std::getenv("PSPRECOMP_GE_GPU_STAGE_VERTICES");
|
|
return text != nullptr && *text != '\0' && std::strcmp(text, "0") != 0;
|
|
}();
|
|
return enabled;
|
|
}
|
|
|
|
|
|
bool gpu_hardware_transform_enabled() noexcept {
|
|
// Rendering.HardwareTransform decides; the environment variable still wins
|
|
// when set, so an A/B in one binary stays possible.
|
|
static const bool enabled = [] {
|
|
const char *text = std::getenv("PSPRECOMP_GE_GPU_HW_TRANSFORM");
|
|
if (text != nullptr && *text != '\0') return std::strcmp(text, "0") != 0;
|
|
const vcs::VcsConfiguration &config = vcs::vcs_configuration();
|
|
return config.initialized && config.rendering.hardware_transform;
|
|
}();
|
|
return enabled;
|
|
}
|
|
|
|
// Off by default: the GPU face-culling path drops whole models.
|
|
//
|
|
// It shipped enabled, but the hardware frontend only covered 5% of city
|
|
// vertices, so the defect was almost invisible. Once the hybrid lit path took
|
|
// coverage to 96.6%, large pieces of scenery disappeared -- confirmed by the
|
|
// user, and confirmed fixed by setting this to 0 with everything else unchanged.
|
|
//
|
|
// The cause is NOT the world matrix handedness. That was tried and it is wrong
|
|
// reasoning: the model-space path and the hybrid path apply exactly the same
|
|
// total transform, only split differently between CPU and shader, so the
|
|
// winding the GPU sees is identical in both. The bug is in this cull path
|
|
// itself and is still unexplained.
|
|
//
|
|
// Culling on the CPU still happens for everything on the legacy path, and the
|
|
// GPU simply rasterizes the back faces it is given -- a fill-rate cost on a
|
|
// discrete GPU that measured well below the win from the hybrid transform.
|
|
//
|
|
// PSPRECOMP_GE_GPU_HW_CULL=1 re-enables it for whoever debugs it next.
|
|
bool gpu_hardware_cull_enabled() noexcept {
|
|
static const bool enabled = [] {
|
|
const char *text = std::getenv("PSPRECOMP_GE_GPU_HW_CULL");
|
|
return text != nullptr && *text != '\0' && std::strcmp(text, "0") != 0;
|
|
}();
|
|
return enabled;
|
|
}
|
|
|
|
// Below this many covered pixels a primitive is not worth waking workers for;
|
|
// the crossover is where dispatch stops being visible next to the covered area.
|
|
std::int64_t parallel_pixel_threshold() noexcept {
|
|
static const std::int64_t threshold = [] {
|
|
constexpr std::int64_t default_value = 2048;
|
|
const char *text = std::getenv("PSPRECOMP_RASTER_PARALLEL_PIXELS");
|
|
if (text == nullptr || *text == '\0') return default_value;
|
|
char *end = nullptr;
|
|
const unsigned long long value = std::strtoull(text, &end, 10);
|
|
if (end == text || *end != '\0' || value > 16'777'216ull) return default_value;
|
|
return static_cast<std::int64_t>(value);
|
|
}();
|
|
return threshold;
|
|
}
|
|
|
|
// Hardware-transform vertex decoding is pure host-side work: each decoded
|
|
// vertex reads immutable guest bytes for the duration of this synchronous GE
|
|
// draw and writes one independent output slot. Large/skinned/lit meshes are
|
|
// therefore safe to fan out across the same persistent host worker pool used
|
|
// by raster/texture work. Keep small draws serial because waking participants
|
|
// costs more than decoding a few dozen simple vertices.
|
|
bool parallel_vertex_decode_enabled() noexcept {
|
|
// Stage 41+ experiment: keep opt-in until physical Windows/Vulkan parity
|
|
// is established against the last known-good Stage 40 run.
|
|
static const bool enabled = [] {
|
|
const char *text = std::getenv("PSPRECOMP_GE_PARALLEL_VERTEX_DECODE");
|
|
return text != nullptr && *text != '\0' && std::strcmp(text, "0") != 0 &&
|
|
std::strcmp(text, "false") != 0 && std::strcmp(text, "FALSE") != 0 &&
|
|
std::strcmp(text, "off") != 0 && std::strcmp(text, "OFF") != 0;
|
|
}();
|
|
return enabled;
|
|
}
|
|
|
|
bool packed_0115_gpu_decode_enabled() noexcept {
|
|
// Stage 45.4: the dominant VCS world format can remain in its original
|
|
// 10-byte PSP encoding all the way to the DX12 input assembler. Unlike
|
|
// the old parallel-decode experiment this removes CPU work instead of
|
|
// distributing it across more host threads. Keep an A/B switch for parity.
|
|
static const bool enabled = [] {
|
|
const char *text = std::getenv("PSPRECOMP_DX12_PACKED_0115");
|
|
if (text == nullptr || *text == '\0') return true;
|
|
return std::strcmp(text, "0") != 0 &&
|
|
std::strcmp(text, "false") != 0 && std::strcmp(text, "FALSE") != 0 &&
|
|
std::strcmp(text, "off") != 0 && std::strcmp(text, "OFF") != 0;
|
|
}();
|
|
return enabled;
|
|
}
|
|
|
|
bool direct_nonindexed_gpu_draw_enabled() noexcept {
|
|
// Stage 43 vkCmdDraw fast path is also isolated behind an explicit switch
|
|
// while the crash fix is validated on the user's physical driver.
|
|
static const bool enabled = [] {
|
|
const char *text = std::getenv("PSPRECOMP_GE_DIRECT_NONINDEXED_DRAW");
|
|
return text != nullptr && *text != '\0' && std::strcmp(text, "0") != 0 &&
|
|
std::strcmp(text, "false") != 0 && std::strcmp(text, "FALSE") != 0 &&
|
|
std::strcmp(text, "off") != 0 && std::strcmp(text, "OFF") != 0;
|
|
}();
|
|
return enabled;
|
|
}
|
|
|
|
std::size_t parallel_vertex_decode_threshold(bool expensive_vertex) noexcept {
|
|
static const std::size_t simple_threshold = [] {
|
|
constexpr std::size_t default_value = 256u;
|
|
const char *text = std::getenv("PSPRECOMP_GE_PARALLEL_VERTEX_THRESHOLD");
|
|
if (text == nullptr || *text == '\0') return default_value;
|
|
char *end = nullptr;
|
|
const unsigned long long value = std::strtoull(text, &end, 10);
|
|
if (end == text || *end != '\0' || value > 1'048'576ull) return default_value;
|
|
return static_cast<std::size_t>(value);
|
|
}();
|
|
// Skinning and CPU lighting perform substantially more math and guest
|
|
// loads per vertex, so their worker crossover is lower.
|
|
return expensive_vertex ? std::max<std::size_t>(64u, simple_threshold / 2u)
|
|
: simple_threshold;
|
|
}
|
|
|
|
unsigned parallel_vertex_decode_max_participants() noexcept {
|
|
// The old experiment could wake every logical CPU (up to 32) for a PSP
|
|
// draw. That creates a synchronization storm on desktop CPUs and made the
|
|
// experiment unnecessarily risky. Stage 45.4 deliberately bounds the
|
|
// default to six participants; the main thread is one of them, so at most
|
|
// five workers are woken. The environment variable remains available for
|
|
// A/B tuning without rebuilding.
|
|
static const unsigned participants = [] {
|
|
constexpr unsigned default_value = 6u;
|
|
const char *text = std::getenv("PSPRECOMP_GE_PARALLEL_VERTEX_MAX_WORKERS");
|
|
if (text == nullptr || *text == '\0') return default_value;
|
|
char *end = nullptr;
|
|
const unsigned long value = std::strtoul(text, &end, 10);
|
|
if (end == text || *end != '\0' || value < 1u || value > 32u)
|
|
return default_value;
|
|
return static_cast<unsigned>(value);
|
|
}();
|
|
return participants;
|
|
}
|
|
|
|
inline void raster_cpu_relax() noexcept {
|
|
#if PSPRECOMP_GE_X86_SIMD
|
|
_mm_pause();
|
|
#else
|
|
// A compiler barrier is enough for the short optimistic spin on platforms
|
|
// without an explicit pause instruction. The loop eventually parks using
|
|
// atomic::wait, so this cannot become an unbounded busy wait.
|
|
std::atomic_signal_fence(std::memory_order_seq_cst);
|
|
#endif
|
|
}
|
|
|
|
struct PixelLoopTimer {
|
|
std::chrono::steady_clock::time_point entry;
|
|
PixelLoopTimer() : entry(std::chrono::steady_clock::now()) {}
|
|
~PixelLoopTimer() {
|
|
g_ge_pixel_ns += static_cast<std::uint64_t>(
|
|
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
|
std::chrono::steady_clock::now() - entry).count());
|
|
}
|
|
};
|
|
|
|
// Scanline-parallel rasterization.
|
|
//
|
|
// Software rasterization is roughly three quarters of a heavy frame and it ran
|
|
// on one core. Inside a single triangle every covered pixel is written exactly
|
|
// once, so splitting its bounding box into horizontal row ranges is order-free:
|
|
// no two workers touch the same pixel, and the result is identical to the
|
|
// serial loop regardless of how the rows are distributed. Ordering between
|
|
// primitives is untouched because each triangle is still finished before the
|
|
// next one starts.
|
|
//
|
|
// PSPRECOMP_RASTER_THREADS overrides the worker count; 1 restores the fully
|
|
// serial path.
|
|
class RowWorkerPool {
|
|
public:
|
|
static constexpr unsigned kMaxThreads = 32u;
|
|
|
|
static RowWorkerPool &instance() {
|
|
static RowWorkerPool pool;
|
|
return pool;
|
|
}
|
|
|
|
[[nodiscard]] unsigned worker_count() const noexcept { return worker_count_; }
|
|
|
|
// Splits [first, last] into small contiguous row ranges and runs `body`
|
|
// across the caller plus worker threads. The callback receives a stable
|
|
// participant slot, so each thread can accumulate statistics without atomics.
|
|
template <typename Body>
|
|
void run(std::int32_t first, std::int32_t last, Body &&body,
|
|
unsigned participant_limit = kMaxThreads) {
|
|
const std::int32_t rows = last - first + 1;
|
|
const unsigned participants = std::min<unsigned>(
|
|
std::min(worker_count_, participant_limit), static_cast<unsigned>(rows));
|
|
if (participants <= 1u || workers_.empty()) {
|
|
body(0u, first, last);
|
|
return;
|
|
}
|
|
|
|
const unsigned chunks = std::min<unsigned>(
|
|
static_cast<unsigned>(rows), participants * kChunksPerParticipant);
|
|
using BodyType = std::remove_reference_t<Body>;
|
|
body_context_ = static_cast<void *>(&body);
|
|
body_invoke_ = [](void *context, unsigned participant,
|
|
std::int32_t begin, std::int32_t end) {
|
|
(*static_cast<BodyType *>(context))(participant, begin, end);
|
|
};
|
|
range_first_ = first;
|
|
rows_per_chunk_ = rows / static_cast<std::int32_t>(chunks);
|
|
remainder_ = rows % static_cast<std::int32_t>(chunks);
|
|
chunk_count_ = chunks;
|
|
active_participants_ = participants;
|
|
next_chunk_.store(0u, std::memory_order_relaxed);
|
|
pending_.store(participants - 1u, std::memory_order_relaxed);
|
|
|
|
// Publishing a new generation releases the callback and range fields.
|
|
// Workers acquire that generation before touching any of them.
|
|
generation_.fetch_add(1u, std::memory_order_release);
|
|
if (parked_.load(std::memory_order_acquire) != 0u)
|
|
generation_.notify_all();
|
|
|
|
run_available_chunks(0u);
|
|
|
|
// A primitive normally completes in tens of microseconds. The caller
|
|
// cannot make progress until every row is done, so parking it in the
|
|
// kernel only adds wake latency. PAUSE keeps this join entirely in user
|
|
// space without the scheduler storm caused by sched_yield.
|
|
while (pending_.load(std::memory_order_acquire) != 0u)
|
|
raster_cpu_relax();
|
|
body_invoke_ = nullptr;
|
|
body_context_ = nullptr;
|
|
}
|
|
|
|
private:
|
|
using BodyInvoke = void (*)(void *, unsigned, std::int32_t, std::int32_t);
|
|
|
|
RowWorkerPool() {
|
|
unsigned requested = std::thread::hardware_concurrency();
|
|
if (requested == 0u) requested = 1u;
|
|
bool explicitly_configured = false;
|
|
if (const char *text = std::getenv("PSPRECOMP_RASTER_THREADS")) {
|
|
char *end = nullptr;
|
|
const unsigned long value = std::strtoul(text, &end, 10);
|
|
if (end != text && *end == '\0' && value >= 1u && value <= kMaxThreads) {
|
|
requested = static_cast<unsigned>(value);
|
|
explicitly_configured = true;
|
|
}
|
|
}
|
|
if (!explicitly_configured) {
|
|
// The guest Allegrex stream is intentionally serial, but host-side
|
|
// decode/raster work is not. Stage 39 capped this pool at eight
|
|
// participants, leaving a large part of 12/16/20/24-thread desktop CPUs
|
|
// idle exactly during texture-streaming spikes. Use every logical
|
|
// CPU up to the pool's conservative hard limit; the caller is one of
|
|
// the participants, so this creates at most kMaxThreads-1 workers.
|
|
requested = std::min(requested, kMaxThreads);
|
|
}
|
|
worker_count_ = std::max(1u, std::min(requested, kMaxThreads));
|
|
if (worker_count_ <= 1u) return;
|
|
workers_.reserve(worker_count_ - 1u);
|
|
for (unsigned index = 1u; index < worker_count_; ++index)
|
|
workers_.emplace_back([this, index] { worker_loop(index); });
|
|
}
|
|
|
|
~RowWorkerPool() {
|
|
stopping_.store(true, std::memory_order_release);
|
|
generation_.fetch_add(1u, std::memory_order_release);
|
|
generation_.notify_all();
|
|
for (std::thread &worker : workers_)
|
|
if (worker.joinable()) worker.join();
|
|
}
|
|
|
|
void run_chunk(unsigned chunk, unsigned participant) {
|
|
const std::int32_t index = static_cast<std::int32_t>(chunk);
|
|
const std::int32_t extra = std::min(index, remainder_);
|
|
const std::int32_t begin = range_first_ + index * rows_per_chunk_ + extra;
|
|
const std::int32_t count = rows_per_chunk_ + (index < remainder_ ? 1 : 0);
|
|
if (count <= 0) return;
|
|
body_invoke_(body_context_, participant, begin, begin + count - 1);
|
|
}
|
|
|
|
void run_available_chunks(unsigned participant) {
|
|
for (;;) {
|
|
const unsigned chunk = next_chunk_.fetch_add(1u, std::memory_order_relaxed);
|
|
if (chunk >= chunk_count_) return;
|
|
run_chunk(chunk, participant);
|
|
}
|
|
}
|
|
|
|
void worker_loop(unsigned worker_index) {
|
|
// Start from generation zero rather than sampling the current value: a
|
|
// worker whose OS thread starts after the first publish must still run
|
|
// that first batch instead of sleeping until the second one.
|
|
std::uint64_t seen = 0u;
|
|
for (;;) {
|
|
unsigned spins = 0u;
|
|
while (generation_.load(std::memory_order_acquire) == seen) {
|
|
if (stopping_.load(std::memory_order_acquire)) return;
|
|
if (spins++ < kWorkerSpins) {
|
|
raster_cpu_relax();
|
|
continue;
|
|
}
|
|
parked_.fetch_add(1u, std::memory_order_release);
|
|
generation_.wait(seen, std::memory_order_acquire);
|
|
parked_.fetch_sub(1u, std::memory_order_release);
|
|
spins = 0u;
|
|
}
|
|
if (stopping_.load(std::memory_order_acquire)) return;
|
|
seen = generation_.load(std::memory_order_acquire);
|
|
|
|
// More chunks than participants balance triangular scanline work:
|
|
// middle rows often contain far more covered pixels than edge rows.
|
|
if (worker_index < active_participants_) {
|
|
run_available_chunks(worker_index);
|
|
if (pending_.fetch_sub(1u, std::memory_order_acq_rel) == 1u)
|
|
pending_.notify_one();
|
|
}
|
|
}
|
|
}
|
|
|
|
static constexpr unsigned kChunksPerParticipant = 4u;
|
|
// Roughly one to a few milliseconds on modern x86. This keeps workers hot
|
|
// across adjacent draw calls, then parks them during real frame idle time.
|
|
static constexpr unsigned kWorkerSpins = 262144u;
|
|
|
|
std::vector<std::thread> workers_;
|
|
void *body_context_{};
|
|
BodyInvoke body_invoke_{};
|
|
std::atomic<unsigned> next_chunk_{};
|
|
std::atomic<unsigned> pending_{};
|
|
std::atomic<unsigned> parked_{};
|
|
unsigned chunk_count_{};
|
|
unsigned active_participants_{};
|
|
std::int32_t range_first_{};
|
|
std::int32_t rows_per_chunk_{};
|
|
std::int32_t remainder_{};
|
|
std::atomic<std::uint64_t> generation_{};
|
|
std::atomic<bool> stopping_{};
|
|
unsigned worker_count_{1u};
|
|
};
|
|
|
|
float decode_float24(std::uint32_t data) noexcept {
|
|
return std::bit_cast<float>((data & 0x00FFFFFFu) << 8u);
|
|
}
|
|
}
|
|
|
|
void reset_ge_transform_state(GeTransformState &state) noexcept {
|
|
state = GeTransformState{};
|
|
for (std::size_t bone = 0; bone < 8u; ++bone) {
|
|
const std::size_t base = bone * 12u;
|
|
state.bones[base + 0u] = 1.0f;
|
|
state.bones[base + 4u] = 1.0f;
|
|
state.bones[base + 8u] = 1.0f;
|
|
}
|
|
state.world[0] = state.world[4] = state.world[8] = 1.0f;
|
|
state.view[0] = state.view[4] = state.view[8] = 1.0f;
|
|
state.texture[0] = state.texture[4] = state.texture[8] = 1.0f;
|
|
state.projection[0] = state.projection[5] = state.projection[10] = state.projection[15] = 1.0f;
|
|
state.morph_weights[0] = 1.0f;
|
|
}
|
|
|
|
void update_ge_transform_state(GeTransformState &state, std::uint32_t command,
|
|
std::uint32_t data) noexcept {
|
|
switch (command) {
|
|
case 0x2Au:
|
|
state.bone_cursor = data & 0x7Fu;
|
|
break;
|
|
case 0x2Bu: {
|
|
// The cursor is seven bits wide, but only 0..95 are backed by matrix
|
|
// storage. Reserved values 96..127 discard writes instead of wrapping
|
|
// through modulo 96 and corrupting the first bones.
|
|
const std::uint32_t index = state.bone_cursor & 0x7Fu;
|
|
if (index < state.bones.size()) state.bones[index] = decode_float24(data);
|
|
state.bone_cursor = (index + 1u) & 0x7Fu;
|
|
break;
|
|
}
|
|
case 0x2Cu: case 0x2Du: case 0x2Eu: case 0x2Fu:
|
|
case 0x30u: case 0x31u: case 0x32u: case 0x33u:
|
|
state.morph_weights[command - 0x2Cu] = decode_float24(data);
|
|
break;
|
|
case 0x3Au:
|
|
state.world_cursor = data & 0xFu;
|
|
break;
|
|
case 0x3Bu: {
|
|
const std::uint32_t index = state.world_cursor & 0xFu;
|
|
if (index < state.world.size()) state.world[index] = decode_float24(data);
|
|
state.world_cursor = (index + 1u) & 0xFu;
|
|
break;
|
|
}
|
|
case 0x3Cu:
|
|
state.view_cursor = data & 0xFu;
|
|
break;
|
|
case 0x3Du: {
|
|
const std::uint32_t index = state.view_cursor & 0xFu;
|
|
if (index < state.view.size()) state.view[index] = decode_float24(data);
|
|
state.view_cursor = (index + 1u) & 0xFu;
|
|
break;
|
|
}
|
|
case 0x3Eu:
|
|
state.projection_cursor = data & 0xFu;
|
|
break;
|
|
case 0x3Fu: {
|
|
const std::uint32_t index = state.projection_cursor & 0xFu;
|
|
state.projection[index] = decode_float24(data);
|
|
state.projection_cursor = (index + 1u) & 0xFu;
|
|
break;
|
|
}
|
|
case 0x40u:
|
|
state.texture_cursor = data & 0xFu;
|
|
break;
|
|
case 0x41u: {
|
|
const std::uint32_t index = state.texture_cursor & 0xFu;
|
|
if (index < state.texture.size()) state.texture[index] = decode_float24(data);
|
|
state.texture_cursor = (index + 1u) & 0xFu;
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
|
|
constexpr std::uint32_t data24(std::uint32_t command) noexcept { return command & 0x00FFFFFFu; }
|
|
constexpr std::uint32_t kVramBase = psprecomp::GuestMemory::kVramPhysicalBase;
|
|
|
|
struct Color {
|
|
std::uint8_t r{255u};
|
|
std::uint8_t g{255u};
|
|
std::uint8_t b{255u};
|
|
std::uint8_t a{255u};
|
|
};
|
|
|
|
struct Vertex {
|
|
float u{};
|
|
float v{};
|
|
float q{1.0f};
|
|
Color color{};
|
|
float x{};
|
|
float y{};
|
|
float z{};
|
|
float w{1.0f};
|
|
float inv_w{1.0f};
|
|
// PSP GE fog coefficient: 1 keeps the fragment color, 0 selects fog RGB.
|
|
float fog_factor{1.0f};
|
|
};
|
|
|
|
thread_local bool g_collect_ge_render_stats = true;
|
|
|
|
struct GeRenderStatsCollectionScope {
|
|
bool previous{};
|
|
explicit GeRenderStatsCollectionScope(bool enabled) noexcept
|
|
: previous(g_collect_ge_render_stats) { g_collect_ge_render_stats = enabled; }
|
|
~GeRenderStatsCollectionScope() { g_collect_ge_render_stats = previous; }
|
|
};
|
|
|
|
void record_clip_vertex(GeRenderStats &stats, const Vertex &vertex) noexcept {
|
|
if (!g_collect_ge_render_stats) return;
|
|
++stats.decoded_vertices;
|
|
const bool finite = std::isfinite(vertex.x) && std::isfinite(vertex.y) &&
|
|
std::isfinite(vertex.z) && std::isfinite(vertex.w);
|
|
if (!finite) {
|
|
++stats.nonfinite_clip_vertices;
|
|
return;
|
|
}
|
|
if (!stats.has_clip_bounds) {
|
|
stats.has_clip_bounds = true;
|
|
stats.clip_min_x = stats.clip_max_x = vertex.x;
|
|
stats.clip_min_y = stats.clip_max_y = vertex.y;
|
|
stats.clip_min_z = stats.clip_max_z = vertex.z;
|
|
stats.clip_min_w = stats.clip_max_w = vertex.w;
|
|
stats.min_abs_w = std::fabs(vertex.w);
|
|
return;
|
|
}
|
|
stats.clip_min_x = std::min(stats.clip_min_x, vertex.x);
|
|
stats.clip_min_y = std::min(stats.clip_min_y, vertex.y);
|
|
stats.clip_min_z = std::min(stats.clip_min_z, vertex.z);
|
|
stats.clip_min_w = std::min(stats.clip_min_w, vertex.w);
|
|
stats.clip_max_x = std::max(stats.clip_max_x, vertex.x);
|
|
stats.clip_max_y = std::max(stats.clip_max_y, vertex.y);
|
|
stats.clip_max_z = std::max(stats.clip_max_z, vertex.z);
|
|
stats.clip_max_w = std::max(stats.clip_max_w, vertex.w);
|
|
stats.min_abs_w = std::min(stats.min_abs_w, std::fabs(vertex.w));
|
|
}
|
|
|
|
void record_screen_vertex(GeRenderStats &stats, const Vertex &vertex) noexcept {
|
|
if (!std::isfinite(vertex.x) || !std::isfinite(vertex.y)) return;
|
|
++stats.screen_vertices;
|
|
const float max_abs = std::max(std::fabs(vertex.x), std::fabs(vertex.y));
|
|
stats.max_abs_screen_coordinate = std::max(stats.max_abs_screen_coordinate, max_abs);
|
|
if (!stats.has_screen_bounds) {
|
|
stats.has_screen_bounds = true;
|
|
stats.screen_min_x = stats.screen_max_x = vertex.x;
|
|
stats.screen_min_y = stats.screen_max_y = vertex.y;
|
|
return;
|
|
}
|
|
stats.screen_min_x = std::min(stats.screen_min_x, vertex.x);
|
|
stats.screen_min_y = std::min(stats.screen_min_y, vertex.y);
|
|
stats.screen_max_x = std::max(stats.screen_max_x, vertex.x);
|
|
stats.screen_max_y = std::max(stats.screen_max_y, vertex.y);
|
|
}
|
|
|
|
struct VertexLayout {
|
|
std::uint32_t type{};
|
|
std::uint32_t tc_type{};
|
|
std::uint32_t color_type{};
|
|
std::uint32_t normal_type{};
|
|
std::uint32_t position_type{};
|
|
std::uint32_t weight_type{};
|
|
std::uint32_t index_type{};
|
|
std::uint32_t weight_count{1u};
|
|
std::uint32_t morph_count{1u};
|
|
std::uint32_t weight_offset{};
|
|
std::uint32_t tc_offset{};
|
|
std::uint32_t color_offset{};
|
|
std::uint32_t normal_offset{};
|
|
std::uint32_t position_offset{};
|
|
std::uint32_t one_size{};
|
|
std::uint32_t stride{};
|
|
bool through{};
|
|
};
|
|
|
|
std::uint32_t align_up(std::uint32_t value, std::uint32_t alignment) noexcept {
|
|
if (alignment <= 1u) return value;
|
|
return (value + alignment - 1u) & ~(alignment - 1u);
|
|
}
|
|
|
|
bool build_vertex_layout(std::uint32_t type, VertexLayout &layout, std::string &error) {
|
|
static constexpr std::array<std::uint32_t, 4> tc_size{0u, 2u, 4u, 8u};
|
|
static constexpr std::array<std::uint32_t, 4> tc_align{1u, 1u, 2u, 4u};
|
|
static constexpr std::array<std::uint32_t, 8> color_size{0u, 0u, 0u, 0u, 2u, 2u, 2u, 4u};
|
|
static constexpr std::array<std::uint32_t, 8> color_align{1u, 1u, 1u, 1u, 2u, 2u, 2u, 4u};
|
|
static constexpr std::array<std::uint32_t, 4> normal_size{0u, 3u, 6u, 12u};
|
|
static constexpr std::array<std::uint32_t, 4> normal_align{1u, 1u, 2u, 4u};
|
|
static constexpr std::array<std::uint32_t, 4> position_size{0u, 3u, 6u, 12u};
|
|
static constexpr std::array<std::uint32_t, 4> position_align{1u, 1u, 2u, 4u};
|
|
static constexpr std::array<std::uint32_t, 4> weight_size{0u, 1u, 2u, 4u};
|
|
static constexpr std::array<std::uint32_t, 4> weight_align{1u, 1u, 2u, 4u};
|
|
|
|
layout = {};
|
|
layout.type = type;
|
|
layout.tc_type = type & 3u;
|
|
layout.color_type = (type >> 2u) & 7u;
|
|
layout.normal_type = (type >> 5u) & 3u;
|
|
layout.position_type = (type >> 7u) & 3u;
|
|
layout.weight_type = (type >> 9u) & 3u;
|
|
layout.index_type = (type >> 11u) & 3u;
|
|
layout.weight_count = ((type >> 14u) & 7u) + 1u;
|
|
layout.morph_count = ((type >> 18u) & 7u) + 1u;
|
|
layout.through = (type & (1u << 23u)) != 0u;
|
|
if (layout.position_type == 0u) {
|
|
error = "GE vertex type has no valid position format";
|
|
return false;
|
|
}
|
|
std::uint32_t offset = 0u;
|
|
if (layout.weight_type != 0u) {
|
|
offset = align_up(offset, weight_align[layout.weight_type]);
|
|
layout.weight_offset = offset;
|
|
offset += weight_size[layout.weight_type] * layout.weight_count;
|
|
}
|
|
offset = align_up(offset, tc_align[layout.tc_type]);
|
|
layout.tc_offset = offset;
|
|
offset += tc_size[layout.tc_type];
|
|
offset = align_up(offset, color_align[layout.color_type]);
|
|
layout.color_offset = offset;
|
|
offset += color_size[layout.color_type];
|
|
offset = align_up(offset, normal_align[layout.normal_type]);
|
|
layout.normal_offset = offset;
|
|
offset += normal_size[layout.normal_type];
|
|
offset = align_up(offset, position_align[layout.position_type]);
|
|
layout.position_offset = offset;
|
|
offset += position_size[layout.position_type];
|
|
|
|
const std::uint32_t final_alignment = std::max({tc_align[layout.tc_type], color_align[layout.color_type],
|
|
normal_align[layout.normal_type], position_align[layout.position_type],
|
|
weight_align[layout.weight_type]});
|
|
layout.one_size = align_up(offset, final_alignment);
|
|
layout.stride = layout.one_size * layout.morph_count;
|
|
return layout.stride != 0u;
|
|
}
|
|
|
|
bool build_vertex_layout_cached(std::uint32_t type, VertexLayout &layout, std::string &error) {
|
|
struct Cache { std::uint32_t type{0xFFFFFFFFu}; VertexLayout layout{}; bool valid{}; };
|
|
static thread_local Cache cache;
|
|
if (cache.valid && cache.type == type) {
|
|
layout = cache.layout;
|
|
return true;
|
|
}
|
|
VertexLayout decoded{};
|
|
if (!build_vertex_layout(type, decoded, error)) return false;
|
|
cache.type = type;
|
|
cache.layout = decoded;
|
|
cache.valid = true;
|
|
layout = decoded;
|
|
return true;
|
|
}
|
|
|
|
std::uint8_t expand4(std::uint32_t value) noexcept {
|
|
value &= 0xFu;
|
|
return static_cast<std::uint8_t>((value << 4u) | value);
|
|
}
|
|
std::uint8_t expand5(std::uint32_t value) noexcept {
|
|
value &= 0x1Fu;
|
|
return static_cast<std::uint8_t>((value << 3u) | (value >> 2u));
|
|
}
|
|
std::uint8_t expand6(std::uint32_t value) noexcept {
|
|
value &= 0x3Fu;
|
|
return static_cast<std::uint8_t>((value << 2u) | (value >> 4u));
|
|
}
|
|
|
|
Color unpack16(std::uint16_t pixel, std::uint32_t format) noexcept {
|
|
switch (format) {
|
|
case 0u: return {expand5(pixel), expand6(pixel >> 5u), expand5(pixel >> 11u), 255u};
|
|
case 1u: return {expand5(pixel), expand5(pixel >> 5u), expand5(pixel >> 10u),
|
|
static_cast<std::uint8_t>((pixel & 0x8000u) ? 255u : 0u)};
|
|
case 2u: return {expand4(pixel), expand4(pixel >> 4u), expand4(pixel >> 8u), expand4(pixel >> 12u)};
|
|
default: return {};
|
|
}
|
|
}
|
|
|
|
Color unpack32(std::uint32_t pixel) noexcept {
|
|
return {static_cast<std::uint8_t>(pixel), static_cast<std::uint8_t>(pixel >> 8u),
|
|
static_cast<std::uint8_t>(pixel >> 16u), static_cast<std::uint8_t>(pixel >> 24u)};
|
|
}
|
|
|
|
std::uint32_t pack32(Color color) noexcept {
|
|
return static_cast<std::uint32_t>(color.r) |
|
|
(static_cast<std::uint32_t>(color.g) << 8u) |
|
|
(static_cast<std::uint32_t>(color.b) << 16u) |
|
|
(static_cast<std::uint32_t>(color.a) << 24u);
|
|
}
|
|
|
|
|
|
std::uint16_t pack16(Color color, std::uint32_t format) noexcept {
|
|
switch (format) {
|
|
case 0u:
|
|
return static_cast<std::uint16_t>((color.r >> 3u) | ((color.g >> 2u) << 5u) | ((color.b >> 3u) << 11u));
|
|
case 1u:
|
|
return static_cast<std::uint16_t>((color.r >> 3u) | ((color.g >> 3u) << 5u) |
|
|
((color.b >> 3u) << 10u) | ((color.a >= 128u ? 1u : 0u) << 15u));
|
|
case 2u:
|
|
return static_cast<std::uint16_t>((color.r >> 4u) | ((color.g >> 4u) << 4u) |
|
|
((color.b >> 4u) << 8u) | ((color.a >> 4u) << 12u));
|
|
default:
|
|
return 0u;
|
|
}
|
|
}
|
|
|
|
Color read_color(const psprecomp::GuestMemory &memory, std::uint32_t address, std::uint32_t format) {
|
|
if (format == 3u) return unpack32(memory.aot_load32(address));
|
|
return unpack16(memory.aot_load16(address), format);
|
|
}
|
|
|
|
// Raw-pointer twins of read_color/write_color. Byte order matches the little
|
|
// endian assembly the checked accessors perform, so results are identical.
|
|
Color read_color_raw(const std::uint8_t *pixel, std::uint32_t format) noexcept {
|
|
if (format == 3u) {
|
|
return unpack32(static_cast<std::uint32_t>(pixel[0]) |
|
|
(static_cast<std::uint32_t>(pixel[1]) << 8u) |
|
|
(static_cast<std::uint32_t>(pixel[2]) << 16u) |
|
|
(static_cast<std::uint32_t>(pixel[3]) << 24u));
|
|
}
|
|
return unpack16(static_cast<std::uint16_t>(
|
|
static_cast<std::uint16_t>(pixel[0]) |
|
|
(static_cast<std::uint16_t>(pixel[1]) << 8u)),
|
|
format);
|
|
}
|
|
|
|
void write_color_raw(std::uint8_t *pixel, std::uint32_t format, Color color,
|
|
std::uint32_t write_mask) noexcept {
|
|
if (format == 3u) {
|
|
const std::uint32_t old = static_cast<std::uint32_t>(pixel[0]) |
|
|
(static_cast<std::uint32_t>(pixel[1]) << 8u) |
|
|
(static_cast<std::uint32_t>(pixel[2]) << 16u) |
|
|
(static_cast<std::uint32_t>(pixel[3]) << 24u);
|
|
const std::uint32_t packed = pack32(color);
|
|
const std::uint32_t value = (old & write_mask) | (packed & ~write_mask);
|
|
pixel[0] = static_cast<std::uint8_t>(value);
|
|
pixel[1] = static_cast<std::uint8_t>(value >> 8u);
|
|
pixel[2] = static_cast<std::uint8_t>(value >> 16u);
|
|
pixel[3] = static_cast<std::uint8_t>(value >> 24u);
|
|
return;
|
|
}
|
|
const Color old = read_color_raw(pixel, format);
|
|
if ((write_mask & 0x000000FFu) != 0u) color.r = old.r;
|
|
if ((write_mask & 0x0000FF00u) != 0u) color.g = old.g;
|
|
if ((write_mask & 0x00FF0000u) != 0u) color.b = old.b;
|
|
if ((write_mask & 0xFF000000u) != 0u) color.a = old.a;
|
|
const std::uint16_t value = pack16(color, format);
|
|
pixel[0] = static_cast<std::uint8_t>(value);
|
|
pixel[1] = static_cast<std::uint8_t>(value >> 8u);
|
|
}
|
|
|
|
void write_color(psprecomp::GuestMemory &memory, std::uint32_t address, std::uint32_t format, Color color,
|
|
std::uint32_t write_mask) {
|
|
if (format == 3u) {
|
|
const std::uint32_t old = memory.aot_load32(address);
|
|
const std::uint32_t packed = pack32(color);
|
|
memory.aot_store32(address, (old & write_mask) | (packed & ~write_mask));
|
|
} else {
|
|
// PSP color masks are expressed in 8888 channels. Convert through the old color so masks
|
|
// remain channel-correct even when the target is packed 16-bit.
|
|
Color old = read_color(memory, address, format);
|
|
if ((write_mask & 0x000000FFu) != 0u) color.r = old.r;
|
|
if ((write_mask & 0x0000FF00u) != 0u) color.g = old.g;
|
|
if ((write_mask & 0x00FF0000u) != 0u) color.b = old.b;
|
|
if ((write_mask & 0xFF000000u) != 0u) color.a = old.a;
|
|
memory.aot_store16(address, pack16(color, format));
|
|
}
|
|
}
|
|
|
|
float signed_normalized8(std::uint8_t value) noexcept {
|
|
return static_cast<float>(static_cast<std::int8_t>(value)) / 128.0f;
|
|
}
|
|
|
|
float signed_normalized16(std::uint16_t value) noexcept {
|
|
return static_cast<float>(static_cast<std::int16_t>(value)) / 32768.0f;
|
|
}
|
|
|
|
struct Vec3 { float x{}, y{}, z{}; };
|
|
struct Vec4 { float x{}, y{}, z{}, w{}; };
|
|
|
|
Vec3 operator+(Vec3 a, Vec3 b) noexcept { return {a.x + b.x, a.y + b.y, a.z + b.z}; }
|
|
Vec3 operator-(Vec3 a, Vec3 b) noexcept { return {a.x - b.x, a.y - b.y, a.z - b.z}; }
|
|
Vec3 operator*(Vec3 a, float scale) noexcept { return {a.x * scale, a.y * scale, a.z * scale}; }
|
|
Vec3 &operator+=(Vec3 &a, Vec3 b) noexcept { a = a + b; return a; }
|
|
float dot(Vec3 a, Vec3 b) noexcept { return a.x * b.x + a.y * b.y + a.z * b.z; }
|
|
|
|
Vec3 normalized_or_zero(Vec3 value) noexcept {
|
|
const float length_squared = dot(value, value);
|
|
if (!std::isfinite(length_squared) || length_squared <= 1.0e-30f) return {};
|
|
const float inverse_length = 1.0f / std::sqrt(length_squared);
|
|
return value * inverse_length;
|
|
}
|
|
|
|
Vec3 normalized_or_001(Vec3 value) noexcept {
|
|
const float length_squared = dot(value, value);
|
|
if (!std::isfinite(length_squared) || length_squared <= 1.0e-30f) return {0.0f, 0.0f, 1.0f};
|
|
const float inverse_length = 1.0f / std::sqrt(length_squared);
|
|
return value * inverse_length;
|
|
}
|
|
|
|
Vec3 transform_4x3(const std::array<float, 12> &matrix, Vec3 value) noexcept {
|
|
return {
|
|
matrix[0] * value.x + matrix[3] * value.y + matrix[6] * value.z + matrix[9],
|
|
matrix[1] * value.x + matrix[4] * value.y + matrix[7] * value.z + matrix[10],
|
|
matrix[2] * value.x + matrix[5] * value.y + matrix[8] * value.z + matrix[11],
|
|
};
|
|
}
|
|
|
|
Vec3 transform_normal_4x3(const std::array<float, 12> &matrix, Vec3 value) noexcept {
|
|
return {
|
|
matrix[0] * value.x + matrix[3] * value.y + matrix[6] * value.z,
|
|
matrix[1] * value.x + matrix[4] * value.y + matrix[7] * value.z,
|
|
matrix[2] * value.x + matrix[5] * value.y + matrix[8] * value.z,
|
|
};
|
|
}
|
|
|
|
Vec4 transform_4x4(const std::array<float, 16> &matrix, Vec3 value) noexcept {
|
|
return {
|
|
matrix[0] * value.x + matrix[4] * value.y + matrix[8] * value.z + matrix[12],
|
|
matrix[1] * value.x + matrix[5] * value.y + matrix[9] * value.z + matrix[13],
|
|
matrix[2] * value.x + matrix[6] * value.y + matrix[10] * value.z + matrix[14],
|
|
matrix[3] * value.x + matrix[7] * value.y + matrix[11] * value.z + matrix[15],
|
|
};
|
|
}
|
|
|
|
|
|
std::array<float, 16> affine_4x3_to_mat4(const std::array<float, 12> &m) noexcept {
|
|
return {m[0], m[1], m[2], 0.0f,
|
|
m[3], m[4], m[5], 0.0f,
|
|
m[6], m[7], m[8], 0.0f,
|
|
m[9], m[10], m[11], 1.0f};
|
|
}
|
|
|
|
std::array<float, 16> multiply_mat4(const std::array<float, 16> &a,
|
|
const std::array<float, 16> &b) noexcept {
|
|
std::array<float, 16> result{};
|
|
for (std::size_t column = 0; column < 4u; ++column) {
|
|
for (std::size_t row = 0; row < 4u; ++row) {
|
|
float sum = 0.0f;
|
|
for (std::size_t k = 0; k < 4u; ++k)
|
|
sum += a[k * 4u + row] * b[column * 4u + k];
|
|
result[column * 4u + row] = sum;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
GeGpuHardwareTransform build_gpu_hardware_transform(
|
|
const std::array<std::uint32_t, 256> &commands,
|
|
const GeTransformState &transform,
|
|
// Hybrid path for lit geometry: the CPU has already applied the world
|
|
// matrix (lighting needs world-space position and normal, and porting the
|
|
// whole PSP T&L into the vertex shader is a much larger and riskier change).
|
|
// The vertices then arrive in world space, so the matrix the shader applies
|
|
// must skip the world stage. Everything downstream -- view, projection,
|
|
// viewport, clipping -- still runs on the GPU, which is where the 90.5% of
|
|
// vertices rejected by the lighting gate were losing it.
|
|
bool vertices_already_in_world_space = false,
|
|
bool texture_coordinates_already_generated = false) noexcept {
|
|
GeGpuHardwareTransform hw{};
|
|
const auto world = vertices_already_in_world_space
|
|
? std::array<float, 16>{1.0f, 0.0f, 0.0f, 0.0f,
|
|
0.0f, 1.0f, 0.0f, 0.0f,
|
|
0.0f, 0.0f, 1.0f, 0.0f,
|
|
0.0f, 0.0f, 0.0f, 1.0f}
|
|
: affine_4x3_to_mat4(transform.world);
|
|
const auto view = affine_4x3_to_mat4(transform.view);
|
|
const auto model_to_view = multiply_mat4(view, world);
|
|
// No widescreen correction on this matrix: transform.projection was already
|
|
// built by the guest with the configured aspect instead of 16/9, so the
|
|
// frustum arrives widened. See the comment in decode_vertex().
|
|
hw.model_to_clip = multiply_mat4(transform.projection, model_to_view);
|
|
hw.model_to_view_z = {model_to_view[2], model_to_view[6],
|
|
model_to_view[10], model_to_view[14]};
|
|
hw.viewport_scale_x = decode_float24(data24(commands[0x42u]));
|
|
hw.viewport_scale_y = decode_float24(data24(commands[0x43u]));
|
|
hw.viewport_scale_z = decode_float24(data24(commands[0x44u]));
|
|
hw.viewport_center_x = decode_float24(data24(commands[0x45u]));
|
|
hw.viewport_center_y = decode_float24(data24(commands[0x46u]));
|
|
hw.viewport_center_z = decode_float24(data24(commands[0x47u]));
|
|
hw.viewport_offset_x = static_cast<float>(data24(commands[0x4Cu]) & 0xFFFFu) / 16.0f;
|
|
hw.viewport_offset_y = static_cast<float>(data24(commands[0x4Du]) & 0xFFFFu) / 16.0f;
|
|
// Matrix/shade UV generation is performed while decoding because it reads
|
|
// model/world normals. Those values are already normalized texture-space
|
|
// coordinates, so applying the ordinary GE scale/offset again would make
|
|
// vehicle environment maps jump between two coordinate systems.
|
|
hw.uv_scale_u = texture_coordinates_already_generated
|
|
? 1.0f : decode_float24(data24(commands[0x48u]));
|
|
hw.uv_scale_v = texture_coordinates_already_generated
|
|
? 1.0f : decode_float24(data24(commands[0x49u]));
|
|
hw.uv_offset_u = texture_coordinates_already_generated
|
|
? 0.0f : decode_float24(data24(commands[0x4Au]));
|
|
hw.uv_offset_v = texture_coordinates_already_generated
|
|
? 0.0f : decode_float24(data24(commands[0x4Bu]));
|
|
hw.fog_end = decode_float24(data24(commands[0xCDu]));
|
|
hw.fog_slope = decode_float24(data24(commands[0xCEu]));
|
|
hw.depth_clip_enabled = (data24(commands[0x1Cu]) & 1u) != 0u;
|
|
hw.cull_enabled = gpu_hardware_cull_enabled() && (data24(commands[0x1Du]) & 1u) != 0u;
|
|
hw.accept_counter_clockwise = (data24(commands[0x9Bu]) & 1u) != 0u;
|
|
|
|
return hw;
|
|
}
|
|
|
|
Vec3 read_vector3(const psprecomp::GuestMemory &memory, std::uint32_t address,
|
|
std::uint32_t format) {
|
|
switch (format) {
|
|
case 1u:
|
|
return {signed_normalized8(memory.aot_load8(address)),
|
|
signed_normalized8(memory.aot_load8(address + 1u)),
|
|
signed_normalized8(memory.aot_load8(address + 2u))};
|
|
case 2u:
|
|
return {signed_normalized16(memory.aot_load16(address)),
|
|
signed_normalized16(memory.aot_load16(address + 2u)),
|
|
signed_normalized16(memory.aot_load16(address + 4u))};
|
|
case 3u:
|
|
return {std::bit_cast<float>(memory.aot_load32(address)),
|
|
std::bit_cast<float>(memory.aot_load32(address + 4u)),
|
|
std::bit_cast<float>(memory.aot_load32(address + 8u))};
|
|
default:
|
|
return {};
|
|
}
|
|
}
|
|
|
|
void read_texcoord(const psprecomp::GuestMemory &memory, std::uint32_t address,
|
|
std::uint32_t format, bool through, float &u, float &v) {
|
|
switch (format) {
|
|
case 0u: u = v = 0.0f; break;
|
|
case 1u:
|
|
u = static_cast<float>(memory.aot_load8(address));
|
|
v = static_cast<float>(memory.aot_load8(address + 1u));
|
|
if (!through) { u *= 1.0f / 128.0f; v *= 1.0f / 128.0f; }
|
|
break;
|
|
case 2u:
|
|
u = static_cast<float>(memory.aot_load16(address));
|
|
v = static_cast<float>(memory.aot_load16(address + 2u));
|
|
if (!through) { u *= 1.0f / 32768.0f; v *= 1.0f / 32768.0f; }
|
|
break;
|
|
case 3u:
|
|
u = std::bit_cast<float>(memory.aot_load32(address));
|
|
v = std::bit_cast<float>(memory.aot_load32(address + 4u));
|
|
break;
|
|
}
|
|
}
|
|
|
|
Color read_vertex_color(const psprecomp::GuestMemory &memory, std::uint32_t address,
|
|
std::uint32_t format) {
|
|
switch (format) {
|
|
case 4u: return unpack16(memory.aot_load16(address), 0u);
|
|
case 5u: return unpack16(memory.aot_load16(address), 1u);
|
|
case 6u: return unpack16(memory.aot_load16(address), 2u);
|
|
case 7u: return unpack32(memory.aot_load32(address));
|
|
default: return {};
|
|
}
|
|
}
|
|
|
|
std::uint8_t clamp_channel(float value) noexcept {
|
|
return static_cast<std::uint8_t>(std::clamp(static_cast<long>(value), 0l, 255l));
|
|
}
|
|
|
|
// A vertex format without a colour component does not mean white. The GE
|
|
// substitutes the material ambient colour (0x55, with 0x58 for alpha), and a
|
|
// game that paints a whole surface one colour has no reason to spend four
|
|
// bytes per vertex repeating it.
|
|
//
|
|
// Defaulting to white is what silvered the sea. VCS draws water as a greyscale
|
|
// wave texture in MODULATE, tinted by the vertex colour: the tiles that carry
|
|
// per-vertex colour came out teal, and the tiles that leave it to the material
|
|
// came out texture x white -- the same texture, the same state, the same draw
|
|
// call, side by side, which is why half the water looked right.
|
|
Color material_ambient_color(const std::array<std::uint32_t, 256> &commands) noexcept {
|
|
const std::uint32_t rgb = data24(commands[0x55u]);
|
|
return Color{
|
|
static_cast<std::uint8_t>(rgb & 0xFFu),
|
|
static_cast<std::uint8_t>((rgb >> 8u) & 0xFFu),
|
|
static_cast<std::uint8_t>((rgb >> 16u) & 0xFFu),
|
|
static_cast<std::uint8_t>(data24(commands[0x58u]) & 0xFFu),
|
|
};
|
|
}
|
|
|
|
Color morph_color(const psprecomp::GuestMemory &memory, std::uint32_t address,
|
|
const VertexLayout &layout, const GeTransformState &transform,
|
|
const std::array<std::uint32_t, 256> &commands) {
|
|
if (layout.color_type < 4u) return material_ambient_color(commands);
|
|
if (layout.morph_count == 1u)
|
|
return read_vertex_color(memory, address + layout.color_offset, layout.color_type);
|
|
|
|
float r = 0.0f, g = 0.0f, b = 0.0f, a = 0.0f;
|
|
for (std::uint32_t morph = 0u; morph < layout.morph_count; ++morph) {
|
|
const Color color = read_vertex_color(memory, address + morph * layout.one_size + layout.color_offset,
|
|
layout.color_type);
|
|
const float weight = transform.morph_weights[morph];
|
|
r += color.r * weight; g += color.g * weight; b += color.b * weight; a += color.a * weight;
|
|
}
|
|
return {clamp_channel(r), clamp_channel(g), clamp_channel(b), clamp_channel(a)};
|
|
}
|
|
|
|
std::array<float, 12> compute_skin_matrix(const psprecomp::GuestMemory &memory,
|
|
std::uint32_t address,
|
|
const VertexLayout &layout,
|
|
const GeTransformState &transform) {
|
|
std::array<float, 12> skin{};
|
|
if (layout.weight_type == 0u) {
|
|
skin[0] = skin[4] = skin[8] = 1.0f;
|
|
return skin;
|
|
}
|
|
|
|
for (std::uint32_t bone = 0u; bone < layout.weight_count; ++bone) {
|
|
float weight = 0.0f;
|
|
switch (layout.weight_type) {
|
|
case 1u: weight = memory.aot_load8(address + layout.weight_offset + bone) * (1.0f / 128.0f); break;
|
|
case 2u: weight = memory.aot_load16(address + layout.weight_offset + bone * 2u) * (1.0f / 32768.0f); break;
|
|
case 3u: weight = std::bit_cast<float>(memory.aot_load32(address + layout.weight_offset + bone * 4u)); break;
|
|
}
|
|
if (weight == 0.0f) continue;
|
|
const std::size_t bone_base = static_cast<std::size_t>(bone) * 12u;
|
|
for (std::size_t element = 0u; element < skin.size(); ++element)
|
|
skin[element] += transform.bones[bone_base + element] * weight;
|
|
}
|
|
return skin;
|
|
}
|
|
|
|
float psp_light_pow(float value, float exponent) noexcept {
|
|
if (exponent <= 0.0f) return 1.0f;
|
|
if (value > 0.0f) return std::pow(value, exponent);
|
|
return value;
|
|
}
|
|
|
|
struct FloatColor { float r{}, g{}, b{}, a{}; };
|
|
|
|
FloatColor to_float_color(Color color) noexcept {
|
|
return {color.r / 255.0f, color.g / 255.0f, color.b / 255.0f, color.a / 255.0f};
|
|
}
|
|
|
|
Color from_float_color(FloatColor color) noexcept {
|
|
auto channel = [](float value) {
|
|
return static_cast<std::uint8_t>(std::clamp(std::lround(value * 255.0f), 0l, 255l));
|
|
};
|
|
return {channel(color.r), channel(color.g), channel(color.b), channel(color.a)};
|
|
}
|
|
|
|
FloatColor rgb_command(std::uint32_t command, float alpha = 1.0f) noexcept {
|
|
const Color color = unpack32(data24(command) | 0xFF000000u);
|
|
return {color.r / 255.0f, color.g / 255.0f, color.b / 255.0f, alpha};
|
|
}
|
|
|
|
FloatColor multiply(FloatColor a, FloatColor b, float scale = 1.0f) noexcept {
|
|
return {a.r * b.r * scale, a.g * b.g * scale, a.b * b.b * scale, a.a * b.a * scale};
|
|
}
|
|
|
|
void add_rgb(FloatColor &destination, FloatColor source) noexcept {
|
|
destination.r += source.r; destination.g += source.g; destination.b += source.b;
|
|
}
|
|
|
|
struct PreparedLight {
|
|
bool enabled{};
|
|
std::uint32_t computation{};
|
|
std::uint32_t type{};
|
|
Vec3 vector{};
|
|
Vec3 attenuation{};
|
|
Vec3 spot_direction{};
|
|
float cutoff{};
|
|
float exponent{};
|
|
FloatColor ambient{};
|
|
FloatColor diffuse{};
|
|
FloatColor specular{};
|
|
};
|
|
|
|
struct PreparedLighting {
|
|
bool enabled{};
|
|
std::uint32_t material_update{};
|
|
float material_alpha{};
|
|
FloatColor material_ambient{};
|
|
FloatColor material_diffuse{};
|
|
FloatColor material_specular{};
|
|
FloatColor emissive{};
|
|
FloatColor global_ambient{};
|
|
float specular_exponent{};
|
|
std::array<PreparedLight, 4> lights{};
|
|
};
|
|
|
|
PreparedLighting prepare_lighting(bool has_vertex_color,
|
|
const std::array<std::uint32_t, 256> &commands) {
|
|
PreparedLighting state{};
|
|
state.enabled = (data24(commands[0x17u]) & 1u) != 0u;
|
|
if (!state.enabled) return state;
|
|
|
|
state.material_update = data24(commands[0x53u]) & (has_vertex_color ? 7u : 0u);
|
|
state.material_alpha = static_cast<float>(data24(commands[0x58u]) & 0xFFu) / 255.0f;
|
|
state.material_ambient = rgb_command(commands[0x55u], state.material_alpha);
|
|
state.material_diffuse = rgb_command(commands[0x56u], state.material_alpha);
|
|
state.material_specular = rgb_command(commands[0x57u], state.material_alpha);
|
|
state.emissive = rgb_command(commands[0x54u], 0.0f);
|
|
const float ambient_alpha = static_cast<float>(data24(commands[0x5Du]) & 0xFFu) / 255.0f;
|
|
state.global_ambient = rgb_command(commands[0x5Cu], ambient_alpha);
|
|
const float exponent = decode_float24(data24(commands[0x5Bu]));
|
|
state.specular_exponent = (!std::isfinite(exponent) || exponent < 0.0f) ? 0.0f : exponent;
|
|
|
|
for (std::uint32_t light = 0u; light < state.lights.size(); ++light) {
|
|
PreparedLight &prepared = state.lights[light];
|
|
prepared.enabled = (data24(commands[0x18u + light]) & 1u) != 0u;
|
|
if (!prepared.enabled) continue;
|
|
const std::uint32_t type_data = data24(commands[0x5Fu + light]);
|
|
prepared.computation = type_data & 3u;
|
|
prepared.type = (type_data >> 8u) & 3u;
|
|
prepared.vector = {
|
|
decode_float24(data24(commands[0x63u + light * 3u])),
|
|
decode_float24(data24(commands[0x64u + light * 3u])),
|
|
decode_float24(data24(commands[0x65u + light * 3u])),
|
|
};
|
|
if (prepared.type == 0u) prepared.vector = normalized_or_001(prepared.vector);
|
|
if (prepared.type != 0u) {
|
|
prepared.attenuation = {
|
|
decode_float24(data24(commands[0x7Bu + light * 3u])),
|
|
decode_float24(data24(commands[0x7Cu + light * 3u])),
|
|
decode_float24(data24(commands[0x7Du + light * 3u])),
|
|
};
|
|
}
|
|
if (prepared.type >= 2u) {
|
|
prepared.spot_direction = normalized_or_001(Vec3{
|
|
decode_float24(data24(commands[0x6Fu + light * 3u])),
|
|
decode_float24(data24(commands[0x70u + light * 3u])),
|
|
decode_float24(data24(commands[0x71u + light * 3u])),
|
|
});
|
|
prepared.cutoff = decode_float24(data24(commands[0x8Bu + light]));
|
|
if (!std::isfinite(prepared.cutoff)) prepared.cutoff = 0.0f;
|
|
prepared.exponent = decode_float24(data24(commands[0x87u + light]));
|
|
if (!std::isfinite(prepared.exponent) || prepared.exponent < 0.0f)
|
|
prepared.exponent = 0.0f;
|
|
}
|
|
prepared.ambient = rgb_command(commands[0x8Fu + light * 3u]);
|
|
prepared.diffuse = rgb_command(commands[0x90u + light * 3u]);
|
|
prepared.specular = rgb_command(commands[0x91u + light * 3u]);
|
|
}
|
|
return state;
|
|
}
|
|
|
|
Color apply_prepared_lighting(Color input, Vec3 world_position, Vec3 world_normal,
|
|
const PreparedLighting &state,
|
|
bool world_normal_is_normalized = false) {
|
|
if (!state.enabled) return input;
|
|
|
|
const std::uint32_t material_update = state.material_update;
|
|
const FloatColor vertex = to_float_color(input);
|
|
const FloatColor material_ambient = (material_update & 1u) != 0u
|
|
? vertex : state.material_ambient;
|
|
const FloatColor material_diffuse = (material_update & 2u) != 0u
|
|
? vertex : state.material_diffuse;
|
|
const FloatColor material_specular = (material_update & 4u) != 0u
|
|
? vertex : state.material_specular;
|
|
|
|
FloatColor result = state.emissive;
|
|
const FloatColor base_ambient = multiply(material_ambient, state.global_ambient);
|
|
add_rgb(result, base_ambient);
|
|
result.a = base_ambient.a;
|
|
|
|
// Most generic paths hand us an arbitrary model/world normal. The common
|
|
// VCS 0x0115 city format has no stored normal at all, however, so its
|
|
// transformed default normal is identical for every vertex in a draw.
|
|
// Stage 45.4 precomputes that normalized vector once and skips the sqrt here
|
|
// for every vertex in the batch.
|
|
if (!world_normal_is_normalized) world_normal = normalized_or_001(world_normal);
|
|
for (const PreparedLight &light : state.lights) {
|
|
if (!light.enabled) continue;
|
|
Vec3 light_vector = light.vector;
|
|
float attenuation_spot = 1.0f;
|
|
if (light.type != 0u) {
|
|
light_vector = light_vector - world_position;
|
|
const float distance_squared = dot(light_vector, light_vector);
|
|
const float distance = std::isfinite(distance_squared) && distance_squared > 0.0f
|
|
? std::sqrt(distance_squared) : 0.0f;
|
|
light_vector = normalized_or_001(light_vector);
|
|
const float denominator = light.attenuation.x + light.attenuation.y * distance +
|
|
light.attenuation.z * distance * distance;
|
|
const float value = denominator > 0.0f ? 1.0f / denominator : 0.0f;
|
|
attenuation_spot = std::clamp(value, 0.0f, 1.0f);
|
|
}
|
|
|
|
if (light.type >= 2u) {
|
|
float raw_spot = dot(light.spot_direction, light_vector);
|
|
if (!std::isfinite(raw_spot)) raw_spot = 0.0f;
|
|
attenuation_spot *= raw_spot >= light.cutoff
|
|
? std::max(0.0f, psp_light_pow(raw_spot, light.exponent)) : 0.0f;
|
|
}
|
|
|
|
add_rgb(result, multiply(light.ambient, material_ambient, attenuation_spot));
|
|
|
|
float diffuse_factor = dot(light_vector, world_normal);
|
|
if (light.computation == 2u)
|
|
diffuse_factor = psp_light_pow(diffuse_factor, state.specular_exponent);
|
|
if (diffuse_factor > 0.0f) {
|
|
add_rgb(result, multiply(light.diffuse, material_diffuse,
|
|
attenuation_spot * diffuse_factor));
|
|
}
|
|
|
|
if (light.computation == 1u && diffuse_factor >= 0.0f) {
|
|
const Vec3 half_vector = normalized_or_001(light_vector + Vec3{0.0f, 0.0f, 1.0f});
|
|
const float specular_factor = psp_light_pow(dot(half_vector, world_normal),
|
|
state.specular_exponent);
|
|
if (specular_factor > 0.0f) {
|
|
add_rgb(result, multiply(light.specular, material_specular,
|
|
attenuation_spot * specular_factor));
|
|
}
|
|
}
|
|
}
|
|
|
|
// This software path currently has one interpolated color. When secondary color
|
|
// mode is selected, folding specular into primary reproduces the final post-texture sum.
|
|
return from_float_color(result);
|
|
}
|
|
|
|
Color apply_lighting(Color input, bool has_vertex_color, Vec3 world_position,
|
|
Vec3 world_normal, const std::array<std::uint32_t, 256> &commands) {
|
|
return apply_prepared_lighting(input, world_position, world_normal,
|
|
prepare_lighting(has_vertex_color, commands));
|
|
}
|
|
|
|
// Stage 45.5: collapse the complete PSP lighting equation into
|
|
// outColor = inColor * mul + add when every enabled light is directional.
|
|
// For vtype 0x0115 there is no stored normal, so the transformed default normal
|
|
// is constant for the whole draw. Directional lights have no position-based
|
|
// attenuation, making every RGB term affine in the incoming vertex colour.
|
|
// The VS quantizes back to RGBA8 before interpolation, matching the CPU path.
|
|
bool prepare_directional_lighting_affine(const PreparedLighting &state,
|
|
Vec3 world_normal,
|
|
std::array<float, 4> &mul,
|
|
std::array<float, 4> &add) noexcept {
|
|
mul = {0.0f, 0.0f, 0.0f, 0.0f};
|
|
add = {state.emissive.r, state.emissive.g, state.emissive.b, 0.0f};
|
|
if (!state.enabled) {
|
|
mul = {1.0f, 1.0f, 1.0f, 1.0f};
|
|
add = {};
|
|
return true;
|
|
}
|
|
for (const PreparedLight &light : state.lights)
|
|
if (light.enabled && light.type != 0u) return false;
|
|
|
|
world_normal = normalized_or_001(world_normal);
|
|
const auto add_term = [&](const FloatColor &material, bool from_vertex,
|
|
const FloatColor &light, float scale) {
|
|
const float coeff[3]{light.r * scale, light.g * scale, light.b * scale};
|
|
const float mat[3]{material.r, material.g, material.b};
|
|
for (int c = 0; c < 3; ++c) {
|
|
if (from_vertex) mul[c] += coeff[c];
|
|
else add[c] += mat[c] * coeff[c];
|
|
}
|
|
};
|
|
|
|
const bool ambient_from_vertex = (state.material_update & 1u) != 0u;
|
|
const bool diffuse_from_vertex = (state.material_update & 2u) != 0u;
|
|
const bool specular_from_vertex = (state.material_update & 4u) != 0u;
|
|
add_term(state.material_ambient, ambient_from_vertex, state.global_ambient, 1.0f);
|
|
if (ambient_from_vertex) mul[3] = state.global_ambient.a;
|
|
else add[3] = state.material_ambient.a * state.global_ambient.a;
|
|
|
|
for (const PreparedLight &light : state.lights) {
|
|
if (!light.enabled) continue;
|
|
add_term(state.material_ambient, ambient_from_vertex, light.ambient, 1.0f);
|
|
float diffuse_factor = dot(light.vector, world_normal);
|
|
if (light.computation == 2u)
|
|
diffuse_factor = psp_light_pow(diffuse_factor, state.specular_exponent);
|
|
if (diffuse_factor > 0.0f)
|
|
add_term(state.material_diffuse, diffuse_from_vertex, light.diffuse, diffuse_factor);
|
|
if (light.computation == 1u && diffuse_factor >= 0.0f) {
|
|
const Vec3 half_vector = normalized_or_001(light.vector + Vec3{0.0f, 0.0f, 1.0f});
|
|
const float specular_factor = psp_light_pow(dot(half_vector, world_normal),
|
|
state.specular_exponent);
|
|
if (specular_factor > 0.0f)
|
|
add_term(state.material_specular, specular_from_vertex,
|
|
light.specular, specular_factor);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool decode_vertex(const psprecomp::GuestMemory &memory, std::uint32_t address,
|
|
const VertexLayout &layout,
|
|
const std::array<std::uint32_t, 256> &commands,
|
|
const GeTransformState &transform,
|
|
Vertex &vertex, std::string &error) {
|
|
if (!memory.contains(address, layout.stride)) {
|
|
error = "GE vertex lies outside guest memory at " + psprecomp::hex32(address);
|
|
return false;
|
|
}
|
|
|
|
if (layout.morph_count == 1u || layout.through) {
|
|
read_texcoord(memory, address + layout.tc_offset, layout.tc_type, layout.through, vertex.u, vertex.v);
|
|
} else {
|
|
vertex.u = vertex.v = 0.0f;
|
|
for (std::uint32_t morph = 0u; morph < layout.morph_count; ++morph) {
|
|
float u = 0.0f, v = 0.0f;
|
|
read_texcoord(memory, address + morph * layout.one_size + layout.tc_offset,
|
|
layout.tc_type, false, u, v);
|
|
vertex.u += u * transform.morph_weights[morph];
|
|
vertex.v += v * transform.morph_weights[morph];
|
|
}
|
|
}
|
|
vertex.color = morph_color(memory, address, layout, transform, commands);
|
|
|
|
if (layout.through) {
|
|
switch (layout.position_type) {
|
|
case 1u:
|
|
vertex.x = vertex.y = vertex.z = 0.0f;
|
|
break;
|
|
case 2u:
|
|
vertex.x = static_cast<float>(static_cast<std::int16_t>(memory.aot_load16(address + layout.position_offset)));
|
|
vertex.y = static_cast<float>(static_cast<std::int16_t>(memory.aot_load16(address + layout.position_offset + 2u)));
|
|
vertex.z = static_cast<float>(memory.aot_load16(address + layout.position_offset + 4u));
|
|
break;
|
|
case 3u:
|
|
vertex.x = std::bit_cast<float>(memory.aot_load32(address + layout.position_offset));
|
|
vertex.y = std::bit_cast<float>(memory.aot_load32(address + layout.position_offset + 4u));
|
|
vertex.z = std::clamp(std::bit_cast<float>(memory.aot_load32(address + layout.position_offset + 8u)),
|
|
0.0f, 65535.0f);
|
|
break;
|
|
default:
|
|
error = "invalid GE position type";
|
|
return false;
|
|
}
|
|
vertex.w = 1.0f;
|
|
vertex.inv_w = 1.0f;
|
|
vertex.fog_factor = 1.0f;
|
|
return true;
|
|
}
|
|
|
|
// The vertex normal is observable only through lighting and through the two
|
|
// UV generation modes that read it. VCS draws its world with prelit vertex
|
|
// colors and lighting disabled, so the normal read, its skin and world
|
|
// matrix transforms and the square-root normalization all produced a value
|
|
// nothing looked at -- once per vertex, about 75000 times per heavy vblank.
|
|
const std::uint32_t uv_generation_data = data24(commands[0xC0u]);
|
|
std::uint32_t uv_generation = uv_generation_data & 3u;
|
|
if (uv_generation == 3u) uv_generation = 0u; // Hardware-compatible fallback for the reserved mode.
|
|
const std::uint32_t uv_generation_source = (uv_generation_data >> 8u) & 3u;
|
|
const bool lighting_enabled = (data24(commands[0x17u]) & 1u) != 0u;
|
|
// Modes 2 and 3 of the matrix generator read the model normal.
|
|
const bool model_normal_needed = lighting_enabled || uv_generation == 2u ||
|
|
(uv_generation == 1u && uv_generation_source >= 2u);
|
|
const bool world_normal_needed = lighting_enabled || uv_generation == 2u;
|
|
|
|
Vec3 model_position{};
|
|
Vec3 model_normal{0.0f, 0.0f, 1.0f};
|
|
const bool read_normal = model_normal_needed && layout.normal_type != 0u;
|
|
if (layout.morph_count == 1u) {
|
|
model_position = read_vector3(memory, address + layout.position_offset, layout.position_type);
|
|
if (read_normal)
|
|
model_normal = read_vector3(memory, address + layout.normal_offset, layout.normal_type);
|
|
} else {
|
|
if (read_normal) model_normal = {};
|
|
for (std::uint32_t morph = 0u; morph < layout.morph_count; ++morph) {
|
|
const std::uint32_t morph_address = address + morph * layout.one_size;
|
|
const float weight = transform.morph_weights[morph];
|
|
model_position += read_vector3(memory, morph_address + layout.position_offset,
|
|
layout.position_type) * weight;
|
|
if (read_normal)
|
|
model_normal += read_vector3(memory, morph_address + layout.normal_offset,
|
|
layout.normal_type) * weight;
|
|
}
|
|
}
|
|
|
|
if (layout.weight_type != 0u) {
|
|
const std::array<float, 12> skin = compute_skin_matrix(memory, address, layout, transform);
|
|
model_position = transform_4x3(skin, model_position);
|
|
if (model_normal_needed) model_normal = transform_normal_4x3(skin, model_normal);
|
|
}
|
|
|
|
const Vec3 world_position = transform_4x3(transform.world, model_position);
|
|
Vec3 world_normal{0.0f, 0.0f, 1.0f};
|
|
if (world_normal_needed) {
|
|
world_normal = transform_normal_4x3(transform.world, model_normal);
|
|
if ((data24(commands[0x51u]) & 1u) != 0u) world_normal = world_normal * -1.0f;
|
|
world_normal = normalized_or_001(world_normal);
|
|
vertex.color = apply_lighting(vertex.color, layout.color_type >= 4u, world_position,
|
|
world_normal, commands);
|
|
}
|
|
|
|
const Vec3 view = transform_4x3(transform.view, world_position);
|
|
if ((data24(commands[0x1Fu]) & 1u) != 0u) {
|
|
const float fog_end = decode_float24(data24(commands[0xCDu]));
|
|
const float fog_slope = decode_float24(data24(commands[0xCEu]));
|
|
const float fog = (view.z + fog_end) * fog_slope;
|
|
vertex.fog_factor = std::isfinite(fog) ? std::clamp(fog, 0.0f, 1.0f) : 1.0f;
|
|
} else {
|
|
vertex.fog_factor = 1.0f;
|
|
}
|
|
const Vec4 clip = transform_4x4(transform.projection, view);
|
|
// Widescreen is not applied here. The projection this multiplies by was
|
|
// already built with the configured aspect instead of 16/9 by the guest
|
|
// itself (get_effective_aspect_ratio in generated/), so the frustum arrives
|
|
// widened; correcting X a second time would double the effect. The 2D
|
|
// interface, which no projection touches, is handled in ge_gpu_backend.cpp.
|
|
vertex.x = clip.x;
|
|
vertex.y = clip.y;
|
|
vertex.z = clip.z;
|
|
vertex.w = clip.w;
|
|
vertex.inv_w = 0.0f;
|
|
|
|
const std::uint32_t texture_size = data24(commands[0xB8u]);
|
|
const float texture_width = static_cast<float>(1u << (texture_size & 0xFu));
|
|
const float texture_height = static_cast<float>(1u << ((texture_size >> 8u) & 0xFu));
|
|
vertex.q = 1.0f;
|
|
if (uv_generation == 1u) {
|
|
Vec3 source{};
|
|
switch (uv_generation_source) {
|
|
case 0u: source = model_position; break;
|
|
case 1u: source = {vertex.u, vertex.v, 0.0f}; break;
|
|
case 2u: source = normalized_or_zero(model_normal); break;
|
|
case 3u: source = model_normal; break;
|
|
}
|
|
const Vec3 stq = transform_4x3(transform.texture, source);
|
|
vertex.u = stq.x * texture_width;
|
|
vertex.v = stq.y * texture_height;
|
|
vertex.q = stq.z;
|
|
} else if (uv_generation == 2u) {
|
|
const std::uint32_t shade = data24(commands[0xC1u]);
|
|
const std::uint32_t light_s = shade & 3u;
|
|
const std::uint32_t light_t = (shade >> 8u) & 3u;
|
|
auto light_vector = [&](std::uint32_t light) {
|
|
return normalized_or_001(Vec3{
|
|
decode_float24(data24(commands[0x63u + light * 3u])),
|
|
decode_float24(data24(commands[0x64u + light * 3u])),
|
|
decode_float24(data24(commands[0x65u + light * 3u])),
|
|
});
|
|
};
|
|
vertex.u = ((dot(light_vector(light_s), world_normal) + 1.0f) * 0.5f) * texture_width;
|
|
vertex.v = ((dot(light_vector(light_t), world_normal) + 1.0f) * 0.5f) * texture_height;
|
|
} else if (layout.tc_type != 0u) {
|
|
const float scale_u = decode_float24(data24(commands[0x48u]));
|
|
const float scale_v = decode_float24(data24(commands[0x49u]));
|
|
const float offset_u = decode_float24(data24(commands[0x4Au]));
|
|
const float offset_v = decode_float24(data24(commands[0x4Bu]));
|
|
vertex.u = (vertex.u * scale_u + offset_u) * texture_width;
|
|
vertex.v = (vertex.v * scale_v + offset_v) * texture_height;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool decode_vertex_0115_fast(const psprecomp::GuestMemory &memory, std::uint32_t address,
|
|
const VertexLayout &layout,
|
|
const std::array<std::uint32_t, 256> &commands,
|
|
const GeTransformState &transform,
|
|
Vertex &vertex, std::string &error) {
|
|
// VCS city geometry is overwhelmingly vtype 0x000115:
|
|
// u8 UV, 5551 colour, no normal/weights, s16 XYZ, one morph. Avoid the
|
|
// generic format switches and unused normal/skinning branches for this exact
|
|
// layout while keeping the same transforms, fog and UV scale/offset.
|
|
if (!memory.contains(address, layout.stride)) {
|
|
error = "GE vertex lies outside guest memory at " + psprecomp::hex32(address);
|
|
return false;
|
|
}
|
|
vertex.u = static_cast<float>(memory.aot_load8(address + layout.tc_offset)) * (1.0f / 128.0f);
|
|
vertex.v = static_cast<float>(memory.aot_load8(address + layout.tc_offset + 1u)) * (1.0f / 128.0f);
|
|
vertex.color = unpack16(memory.aot_load16(address + layout.color_offset), 1u);
|
|
const Vec3 model_position{
|
|
signed_normalized16(memory.aot_load16(address + layout.position_offset)),
|
|
signed_normalized16(memory.aot_load16(address + layout.position_offset + 2u)),
|
|
signed_normalized16(memory.aot_load16(address + layout.position_offset + 4u)),
|
|
};
|
|
const Vec3 world_position = transform_4x3(transform.world, model_position);
|
|
const Vec3 view = transform_4x3(transform.view, world_position);
|
|
if ((data24(commands[0x1Fu]) & 1u) != 0u) {
|
|
const float fog_end = decode_float24(data24(commands[0xCDu]));
|
|
const float fog_slope = decode_float24(data24(commands[0xCEu]));
|
|
const float fog = (view.z + fog_end) * fog_slope;
|
|
vertex.fog_factor = std::isfinite(fog) ? std::clamp(fog, 0.0f, 1.0f) : 1.0f;
|
|
} else {
|
|
vertex.fog_factor = 1.0f;
|
|
}
|
|
const Vec4 clip = transform_4x4(transform.projection, view);
|
|
// See decode_vertex(): widescreen is already in transform.projection.
|
|
vertex.x = clip.x;
|
|
vertex.y = clip.y;
|
|
vertex.z = clip.z;
|
|
vertex.w = clip.w;
|
|
vertex.inv_w = 0.0f;
|
|
vertex.q = 1.0f;
|
|
|
|
const std::uint32_t texture_size = data24(commands[0xB8u]);
|
|
const float texture_width = static_cast<float>(1u << (texture_size & 0xFu));
|
|
const float texture_height = static_cast<float>(1u << ((texture_size >> 8u) & 0xFu));
|
|
const float scale_u = decode_float24(data24(commands[0x48u]));
|
|
const float scale_v = decode_float24(data24(commands[0x49u]));
|
|
const float offset_u = decode_float24(data24(commands[0x4Au]));
|
|
const float offset_v = decode_float24(data24(commands[0x4Bu]));
|
|
vertex.u = (vertex.u * scale_u + offset_u) * texture_width;
|
|
vertex.v = (vertex.v * scale_v + offset_v) * texture_height;
|
|
return true;
|
|
}
|
|
|
|
bool decode_vertex_optimized(const psprecomp::GuestMemory &memory, std::uint32_t address,
|
|
const VertexLayout &layout,
|
|
const std::array<std::uint32_t, 256> &commands,
|
|
const GeTransformState &transform,
|
|
Vertex &vertex, std::string &error) {
|
|
const std::uint32_t uv_mode = data24(commands[0xC0u]) & 3u;
|
|
if (layout.type == 0x000115u && !layout.through &&
|
|
(data24(commands[0x17u]) & 1u) == 0u && (uv_mode == 0u || uv_mode == 3u))
|
|
return decode_vertex_0115_fast(memory, address, layout, commands, transform, vertex, error);
|
|
return decode_vertex(memory, address, layout, commands, transform, vertex, error);
|
|
}
|
|
|
|
|
|
bool decode_model_vertex_0115_for_gpu_fast(
|
|
const psprecomp::GuestMemory &memory, std::uint32_t address,
|
|
const VertexLayout &layout, const GeTransformState &transform,
|
|
const std::array<std::uint32_t, 256> &commands,
|
|
bool lighting_enabled, const PreparedLighting *prepared_lighting,
|
|
GeGpuVertex &vertex, std::string &error,
|
|
const std::uint8_t *prevalidated_raw = nullptr,
|
|
const Vec3 *precomputed_world_normal = nullptr) {
|
|
// Common VCS world layout: u8 UV + 5551 colour + s16 XYZ, no normal,
|
|
// no weights, one morph. The generic decoder performs several format
|
|
// switches and six separately bounds-checked guest loads per vertex.
|
|
// Resolve the packed vertex once and decode straight from the host pointer.
|
|
// Contiguous GPU draws validate the entire source span once before the
|
|
// worker loop and pass a direct pointer here. This avoids canonicalization,
|
|
// region selection and bounds checking once per vertex on the dominant city
|
|
// format. Sparse callers retain the exact old checked path.
|
|
const std::uint8_t *raw = prevalidated_raw != nullptr
|
|
? prevalidated_raw : memory.raw_pointer(address, layout.stride);
|
|
if (raw == nullptr) {
|
|
error = "GE hardware-transform vertex lies outside guest memory at " +
|
|
psprecomp::hex32(address);
|
|
return false;
|
|
}
|
|
const auto le16 = [raw](std::uint32_t offset) noexcept {
|
|
return static_cast<std::uint16_t>(raw[offset]) |
|
|
(static_cast<std::uint16_t>(raw[offset + 1u]) << 8u);
|
|
};
|
|
|
|
const float u = static_cast<float>(raw[layout.tc_offset]) * (1.0f / 128.0f);
|
|
const float v = static_cast<float>(raw[layout.tc_offset + 1u]) * (1.0f / 128.0f);
|
|
const Color color = unpack16(le16(layout.color_offset), 1u);
|
|
Vec3 model_position{
|
|
signed_normalized16(le16(layout.position_offset)),
|
|
signed_normalized16(le16(layout.position_offset + 2u)),
|
|
signed_normalized16(le16(layout.position_offset + 4u)),
|
|
};
|
|
|
|
Color final_color = color;
|
|
if (lighting_enabled) {
|
|
const Vec3 world_position = transform_4x3(transform.world, model_position);
|
|
Vec3 world_normal{};
|
|
if (precomputed_world_normal != nullptr) {
|
|
world_normal = *precomputed_world_normal;
|
|
} else {
|
|
world_normal = transform_normal_4x3(
|
|
transform.world, Vec3{0.0f, 0.0f, 1.0f});
|
|
if ((data24(commands[0x51u]) & 1u) != 0u) world_normal = world_normal * -1.0f;
|
|
world_normal = normalized_or_001(world_normal);
|
|
}
|
|
final_color = prepared_lighting
|
|
? apply_prepared_lighting(color, world_position, world_normal,
|
|
*prepared_lighting, true)
|
|
: apply_lighting(color, true, world_position, world_normal, commands);
|
|
model_position = world_position;
|
|
}
|
|
|
|
vertex = {};
|
|
vertex.x = model_position.x;
|
|
vertex.y = model_position.y;
|
|
vertex.z = model_position.z;
|
|
vertex.w = 1.0f;
|
|
vertex.u = u;
|
|
vertex.v = v;
|
|
vertex.q = 1.0f;
|
|
vertex.fog_factor = 1.0f;
|
|
vertex.rgba = static_cast<std::uint32_t>(final_color.r) |
|
|
(static_cast<std::uint32_t>(final_color.g) << 8u) |
|
|
(static_cast<std::uint32_t>(final_color.b) << 16u) |
|
|
(static_cast<std::uint32_t>(final_color.a) << 24u);
|
|
return true;
|
|
}
|
|
|
|
bool decode_model_vertex_for_gpu(const psprecomp::GuestMemory &memory,
|
|
std::uint32_t address,
|
|
const VertexLayout &layout,
|
|
const GeTransformState &transform,
|
|
const std::array<std::uint32_t, 256> &commands,
|
|
bool lighting_enabled,
|
|
const PreparedLighting *prepared_lighting,
|
|
std::uint32_t uv_generation,
|
|
GeGpuVertex &vertex,
|
|
std::string &error) {
|
|
// The common VCS city vertex format does not need the generic decoder at
|
|
// all when UV generation is direct. Keep lighting support so the same
|
|
// specialization covers lit and unlit opaque world geometry.
|
|
if (layout.type == 0x000115u && !layout.through &&
|
|
layout.morph_count == 1u && layout.weight_type == 0u &&
|
|
layout.normal_type == 0u && uv_generation == 0u) {
|
|
return decode_model_vertex_0115_for_gpu_fast(
|
|
memory, address, layout, transform, commands, lighting_enabled,
|
|
prepared_lighting, vertex, error);
|
|
}
|
|
|
|
if (!memory.contains(address, layout.stride)) {
|
|
error = "GE hardware-transform vertex lies outside guest memory at " +
|
|
psprecomp::hex32(address);
|
|
return false;
|
|
}
|
|
|
|
float u = 0.0f, v = 0.0f;
|
|
if (layout.morph_count == 1u) {
|
|
read_texcoord(memory, address + layout.tc_offset, layout.tc_type, false, u, v);
|
|
} else {
|
|
for (std::uint32_t morph = 0u; morph < layout.morph_count; ++morph) {
|
|
float mu = 0.0f, mv = 0.0f;
|
|
read_texcoord(memory, address + morph * layout.one_size + layout.tc_offset,
|
|
layout.tc_type, false, mu, mv);
|
|
u += mu * transform.morph_weights[morph];
|
|
v += mv * transform.morph_weights[morph];
|
|
}
|
|
}
|
|
|
|
const Color color = morph_color(memory, address, layout, transform, commands);
|
|
Vec3 model_position{};
|
|
if (layout.morph_count == 1u) {
|
|
model_position = read_vector3(memory, address + layout.position_offset,
|
|
layout.position_type);
|
|
} else {
|
|
for (std::uint32_t morph = 0u; morph < layout.morph_count; ++morph) {
|
|
model_position += read_vector3(
|
|
memory, address + morph * layout.one_size + layout.position_offset,
|
|
layout.position_type) * transform.morph_weights[morph];
|
|
}
|
|
}
|
|
const std::uint32_t uv_generation_source =
|
|
(data24(commands[0xC0u]) >> 8u) & 3u;
|
|
const bool model_normal_needed = lighting_enabled || uv_generation == 2u ||
|
|
(uv_generation == 1u && uv_generation_source >= 2u);
|
|
Vec3 model_normal{0.0f, 0.0f, 1.0f};
|
|
if (model_normal_needed && layout.normal_type != 0u)
|
|
model_normal = read_vector3(memory, address + layout.normal_offset,
|
|
layout.normal_type);
|
|
|
|
if (layout.weight_type != 0u) {
|
|
const std::array<float, 12> skin = compute_skin_matrix(memory, address, layout, transform);
|
|
model_position = transform_4x3(skin, model_position);
|
|
if (model_normal_needed)
|
|
model_normal = transform_normal_4x3(skin, model_normal);
|
|
}
|
|
|
|
// Generate reflection/matrix UVs on the CPU just like decode_vertex().
|
|
// Previously these passes stayed on the legacy transform while the opaque
|
|
// vehicle body used Vulkan, causing tiny depth disagreements and visible
|
|
// flashing between the two layers.
|
|
float generated_q = 1.0f;
|
|
if (uv_generation == 1u) {
|
|
Vec3 source{};
|
|
switch (uv_generation_source) {
|
|
case 0u: source = model_position; break;
|
|
case 1u: source = {u, v, 0.0f}; break;
|
|
case 2u: source = normalized_or_zero(model_normal); break;
|
|
case 3u: source = model_normal; break;
|
|
}
|
|
const Vec3 stq = transform_4x3(transform.texture, source);
|
|
u = stq.x;
|
|
v = stq.y;
|
|
generated_q = stq.z;
|
|
} else if (uv_generation == 2u) {
|
|
Vec3 world_normal = transform_normal_4x3(transform.world, model_normal);
|
|
if ((data24(commands[0x51u]) & 1u) != 0u) world_normal = world_normal * -1.0f;
|
|
world_normal = normalized_or_001(world_normal);
|
|
const std::uint32_t shade = data24(commands[0xC1u]);
|
|
const std::uint32_t light_s = shade & 3u;
|
|
const std::uint32_t light_t = (shade >> 8u) & 3u;
|
|
const auto light_vector = [&](std::uint32_t light) {
|
|
return normalized_or_001(Vec3{
|
|
decode_float24(data24(commands[0x63u + light * 3u])),
|
|
decode_float24(data24(commands[0x64u + light * 3u])),
|
|
decode_float24(data24(commands[0x65u + light * 3u])),
|
|
});
|
|
};
|
|
u = (dot(light_vector(light_s), world_normal) + 1.0f) * 0.5f;
|
|
v = (dot(light_vector(light_t), world_normal) + 1.0f) * 0.5f;
|
|
}
|
|
|
|
// Hybrid path for lit draws. A gate census over the capture route found
|
|
// that 15823949 of 17481629 vertices -- 90.5% -- were refused by the
|
|
// hardware frontend for one reason only: lighting enabled. Lighting needs
|
|
// world-space position and normal, so the world matrix has to stay on the
|
|
// CPU; view, projection, viewport and clipping do not, and those are what
|
|
// triprep and two thirds of the matrix work cost.
|
|
//
|
|
// The vertex colour is computed by exactly the same apply_lighting() the
|
|
// legacy path uses, so colour output is unchanged -- only where the
|
|
// remaining matrices are applied moves.
|
|
Color final_color = color;
|
|
if (lighting_enabled) {
|
|
// Same default decode_vertex() uses when the vertex format carries no
|
|
// normal; the GE still lights those vertices.
|
|
const Vec3 world_position = transform_4x3(transform.world, model_position);
|
|
Vec3 world_normal = transform_normal_4x3(transform.world, model_normal);
|
|
if ((data24(commands[0x51u]) & 1u) != 0u) world_normal = world_normal * -1.0f;
|
|
world_normal = normalized_or_001(world_normal);
|
|
final_color = prepared_lighting
|
|
? apply_prepared_lighting(color, world_position, world_normal, *prepared_lighting)
|
|
: apply_lighting(color, layout.color_type >= 4u, world_position,
|
|
world_normal, commands);
|
|
model_position = world_position;
|
|
}
|
|
|
|
vertex = {};
|
|
vertex.x = model_position.x;
|
|
vertex.y = model_position.y;
|
|
vertex.z = model_position.z;
|
|
vertex.w = 1.0f;
|
|
vertex.u = u;
|
|
vertex.v = v;
|
|
vertex.q = generated_q;
|
|
vertex.fog_factor = 1.0f;
|
|
vertex.rgba = static_cast<std::uint32_t>(final_color.r) |
|
|
(static_cast<std::uint32_t>(final_color.g) << 8u) |
|
|
(static_cast<std::uint32_t>(final_color.b) << 16u) |
|
|
(static_cast<std::uint32_t>(final_color.a) << 24u);
|
|
return true;
|
|
}
|
|
|
|
std::uint32_t framebuffer_address(const std::array<std::uint32_t, 256> &commands) noexcept {
|
|
return kVramBase | (data24(commands[0x9Cu]) & 0x001FFFF0u);
|
|
}
|
|
std::uint32_t depthbuffer_address(const std::array<std::uint32_t, 256> &commands) noexcept {
|
|
return kVramBase | (data24(commands[0x9Eu]) & 0x001FFFF0u);
|
|
}
|
|
std::uint32_t texture_address(const std::array<std::uint32_t, 256> &commands,
|
|
std::uint32_t level) noexcept {
|
|
level = std::min(level, 7u);
|
|
return (data24(commands[0xA0u + level]) & 0x00FFFFF0u) |
|
|
((data24(commands[0xA8u + level]) << 8u) & 0x0F000000u);
|
|
}
|
|
std::uint32_t texture_address(const std::array<std::uint32_t, 256> &commands) noexcept {
|
|
return texture_address(commands, 0u);
|
|
}
|
|
|
|
// Whether the CPU rasterizer can leave this surface alone.
|
|
//
|
|
// With the Vulkan backend on, every primitive is rasterized twice: here on the
|
|
// CPU into guest VRAM, and again on the GPU. The CPU copy still matters for
|
|
// surfaces the game samples back, but not for the one the swapchain is showing
|
|
// -- nothing reads those pixels. Skipping just that surface keeps VRAM correct
|
|
// everywhere else.
|
|
//
|
|
// The same applies to the surface the guest handed to sceDisplaySetFrameBuf
|
|
// while the backend presents its own image. VCS composes into it with 64
|
|
// through-mode rectangles that cost about 5.6 ms per rendered frame, and the
|
|
// composition never reaches the screen on the swapchain path.
|
|
//
|
|
// That rests on two conditions, and both are counted rather than assumed --
|
|
// report_present_stats() prints them at exit:
|
|
//
|
|
// display_fb_sampled_draws nothing may sample the displayed surface back as
|
|
// a texture, or the skipped pixels become visible
|
|
// software_after_gpu the run must never return to presenting that
|
|
// surface by GDI after the swapchain took over,
|
|
// which is when a stale buffer would reach the
|
|
// window
|
|
//
|
|
// Both were zero over a 4000-vblank scripted route and over a two-minute
|
|
// interactive session. If either ever becomes nonzero, set
|
|
// PSPRECOMP_GE_GPU_SKIP_DISPLAYED_RASTER=0 and say so in the handoff. The
|
|
// structural fix that removes the conditions entirely is the GPU-to-GPU path.
|
|
bool software_raster_skipped(const std::array<std::uint32_t, 256> &commands) noexcept {
|
|
static const bool skip_everything = [] {
|
|
const char *value = std::getenv("PSPRECOMP_GE_GPU_SKIP_SOFTWARE_RASTER");
|
|
if (value != nullptr && *value != '\0') return *value != '0';
|
|
// Native DX12 GE is authoritative. The first physical Stage 44.6 run
|
|
// exposed that the old conservative policy still CPU-rasterized most
|
|
// offscreen targets as well as submitting the same geometry to D3D12.
|
|
// That double raster is unnecessary once DX12GEColor is enabled.
|
|
const VcsConfiguration &cfg = vcs_configuration();
|
|
return cfg.initialized && cfg.rendering.backend == RenderingBackend::DirectX12 &&
|
|
cfg.rendering.dx12_ge_color;
|
|
}();
|
|
static const bool skip_owned = [] {
|
|
const char *value = std::getenv("PSPRECOMP_GE_GPU_SKIP_OWNED_RASTER");
|
|
return value == nullptr || (*value != '\0' && *value != '0');
|
|
}();
|
|
static const bool skip_displayed = [] {
|
|
const char *value = std::getenv("PSPRECOMP_GE_GPU_SKIP_DISPLAYED_RASTER");
|
|
return value == nullptr || (*value != '\0' && *value != '0');
|
|
}();
|
|
if (!ge_gpu_backend_active()) return false;
|
|
if (skip_everything) return true;
|
|
const std::uint32_t target = framebuffer_address(commands) & 0x001FFFF0u;
|
|
if (skip_owned) {
|
|
const std::uint32_t owned = ge_gpu_backend_owned_framebuffer();
|
|
if (owned != 0u && target == owned) return true;
|
|
}
|
|
if (skip_displayed && ge_gpu_backend_presents_directly()) {
|
|
const std::uint32_t displayed = ge_gpu_backend_display_framebuffer();
|
|
if (displayed != 0u && target == displayed) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
std::int32_t signed_texture_lod_offset16(std::uint32_t texlevel) noexcept {
|
|
const std::uint32_t raw = (texlevel >> 16u) & 0xFFu;
|
|
return (raw & 0x80u) != 0u ? static_cast<std::int32_t>(raw) - 256
|
|
: static_cast<std::int32_t>(raw);
|
|
}
|
|
|
|
std::uint32_t selected_texture_level(const std::array<std::uint32_t, 256> &commands) noexcept {
|
|
const std::uint32_t texfilter = data24(commands[0xC6u]);
|
|
const bool mip_enabled = (texfilter & 4u) != 0u;
|
|
const std::uint32_t max_level = (data24(commands[0xC2u]) >> 16u) & 7u;
|
|
if (!mip_enabled || max_level == 0u) return 0u;
|
|
const std::uint32_t texlevel = data24(commands[0xC8u]);
|
|
const std::uint32_t mode = texlevel & 3u;
|
|
// PSP mode 1 is a constant 4.4 fixed-point LOD. Automatic/slope modes need
|
|
// derivatives from the final rasterizer, so they correctly stay at level 0
|
|
// until the full image mip chain is uploaded in a later physical-GPU pass.
|
|
if (mode != 1u) return 0u;
|
|
const float lod = static_cast<float>(signed_texture_lod_offset16(texlevel)) / 16.0f;
|
|
const bool mip_linear = (texfilter & 2u) != 0u;
|
|
const int selected = mip_linear ? static_cast<int>(std::floor(lod))
|
|
: static_cast<int>(std::floor(lod + 0.5f));
|
|
return static_cast<std::uint32_t>(std::clamp(selected, 0, static_cast<int>(max_level)));
|
|
}
|
|
std::uint32_t clut_address(const std::array<std::uint32_t, 256> &commands) noexcept {
|
|
return (data24(commands[0xB0u]) & 0x00FFFFF0u) | ((data24(commands[0xB1u]) << 8u) & 0x0F000000u);
|
|
}
|
|
|
|
std::uint32_t bytes_per_pixel(std::uint32_t format) noexcept { return format == 3u ? 4u : 2u; }
|
|
|
|
std::uint32_t swizzled_offset(std::uint32_t byte_x, std::uint32_t y, std::uint32_t row_bytes) noexcept {
|
|
const std::uint32_t blocks_per_row = (row_bytes + 15u) / 16u;
|
|
const std::uint32_t block_x = byte_x / 16u;
|
|
const std::uint32_t block_y = y / 8u;
|
|
return (block_y * blocks_per_row + block_x) * 128u + (y & 7u) * 16u + (byte_x & 15u);
|
|
}
|
|
|
|
std::uint32_t wrap_coord(std::int32_t value, std::uint32_t dimension, bool clamp) noexcept {
|
|
if (dimension == 0u) return 0u;
|
|
if (clamp) return static_cast<std::uint32_t>(std::clamp<std::int32_t>(value, 0, static_cast<std::int32_t>(dimension - 1u)));
|
|
// GE texture dimensions are powers of two. Masking gives the same wrapped
|
|
// result for positive and negative two's-complement coordinates without an
|
|
// integer division in every nearest sample (four times for bilinear).
|
|
if ((dimension & (dimension - 1u)) == 0u)
|
|
return static_cast<std::uint32_t>(value) & (dimension - 1u);
|
|
const std::int32_t d = static_cast<std::int32_t>(dimension);
|
|
std::int32_t result = value % d;
|
|
if (result < 0) result += d;
|
|
return static_cast<std::uint32_t>(result);
|
|
}
|
|
|
|
Color read_clut(const psprecomp::GuestMemory &memory, const std::array<std::uint32_t, 256> &commands,
|
|
std::uint32_t raw_index) {
|
|
const std::uint32_t format_data = data24(commands[0xC5u]);
|
|
const std::uint32_t palette_format = format_data & 3u;
|
|
const std::uint32_t shift = (format_data >> 2u) & 0x1Fu;
|
|
const std::uint32_t mask = (format_data >> 8u) & 0xFFu;
|
|
const std::uint32_t start = ((format_data >> 16u) & 0x1Fu) << 4u;
|
|
const std::uint32_t wrap_mask = palette_format == 3u ? 0xFFu : 0x1FFu;
|
|
const std::uint32_t index = (((raw_index >> shift) & mask) | (start & wrap_mask)) & wrap_mask;
|
|
const std::uint32_t address = clut_address(commands) + index * (palette_format == 3u ? 4u : 2u);
|
|
if (!memory.contains(address, palette_format == 3u ? 4u : 2u)) return {};
|
|
if (palette_format == 3u) return unpack32(memory.aot_load32(address));
|
|
return unpack16(memory.aot_load16(address), palette_format);
|
|
}
|
|
|
|
// Per-primitive texture state.
|
|
//
|
|
// A bilinear fetch calls the nearest sampler four times, and each call used to
|
|
// re-decode the texture size, wrap mode, pixel format, buffer width, swizzle
|
|
// flag and base address, then take a bounds-checked guest load -- plus the same
|
|
// again inside read_clut for palettised formats. None of it varies within a
|
|
// primitive, so it is decoded once and the sampler becomes address arithmetic
|
|
// over a raw pointer.
|
|
struct TextureSetup {
|
|
const std::uint8_t *pixels{};
|
|
const std::uint8_t *clut_pixels{};
|
|
std::uint32_t base{};
|
|
std::uint32_t clut_base{};
|
|
std::uint32_t width{};
|
|
std::uint32_t height{};
|
|
std::uint32_t buffer_width{};
|
|
std::uint32_t format{};
|
|
std::uint32_t clut_format{};
|
|
std::uint32_t clut_shift{};
|
|
std::uint32_t clut_mask{};
|
|
std::uint32_t clut_start{};
|
|
std::uint32_t clut_wrap_mask{};
|
|
std::uint32_t clut_entry_bytes{};
|
|
bool swizzled{};
|
|
bool clamp_u{};
|
|
bool clamp_v{};
|
|
bool linear{};
|
|
std::uint32_t selected_level{};
|
|
};
|
|
|
|
TextureSetup make_texture_setup_for_level(const psprecomp::GuestMemory &memory, const std::array<std::uint32_t, 256> &commands, std::uint32_t requested_level) noexcept {
|
|
TextureSetup setup{}; setup.selected_level=std::min<std::uint32_t>(requested_level,7u);
|
|
const std::uint32_t size = data24(commands[0xB8u + setup.selected_level]);
|
|
setup.width = 1u << (size & 0xFu);
|
|
setup.height = 1u << ((size >> 8u) & 0xFu);
|
|
const std::uint32_t wrap = data24(commands[0xC7u]);
|
|
setup.clamp_u = (wrap & 1u) != 0u;
|
|
setup.clamp_v = (wrap & 0x100u) != 0u;
|
|
setup.format = data24(commands[0xC3u]) & 0xFu;
|
|
setup.buffer_width = std::max<std::uint32_t>(
|
|
1u, data24(commands[0xA8u + setup.selected_level]) & 0x7FFu);
|
|
setup.swizzled = (data24(commands[0xC2u]) & 1u) != 0u;
|
|
setup.base = texture_address(commands, setup.selected_level);
|
|
setup.linear = ((data24(commands[0xC6u]) >> 8u) & 1u) != 0u;
|
|
|
|
const std::uint32_t clut_data = data24(commands[0xC5u]);
|
|
setup.clut_format = clut_data & 3u;
|
|
setup.clut_shift = (clut_data >> 2u) & 0x1Fu;
|
|
setup.clut_mask = (clut_data >> 8u) & 0xFFu;
|
|
setup.clut_start = ((clut_data >> 16u) & 0x1Fu) << 4u;
|
|
setup.clut_wrap_mask = setup.clut_format == 3u ? 0xFFu : 0x1FFu;
|
|
setup.clut_entry_bytes = setup.clut_format == 3u ? 4u : 2u;
|
|
setup.clut_base = clut_address(commands);
|
|
|
|
// Bound the texture by the largest offset the swizzled or linear layout can
|
|
// produce for this size, so a single resolve covers every texel fetched.
|
|
const std::uint32_t row_bytes = setup.buffer_width * 4u;
|
|
const std::uint64_t span = static_cast<std::uint64_t>(setup.height + 8u) * (row_bytes + 128u);
|
|
setup.pixels = memory.raw_pointer(setup.base, static_cast<std::size_t>(span));
|
|
setup.clut_pixels = memory.raw_pointer(setup.clut_base,
|
|
(setup.clut_wrap_mask + 1u) * setup.clut_entry_bytes);
|
|
return setup;
|
|
}
|
|
|
|
TextureSetup make_texture_setup(const psprecomp::GuestMemory &memory, const std::array<std::uint32_t, 256> &commands) noexcept { return make_texture_setup_for_level(memory,commands,selected_texture_level(commands)); }
|
|
|
|
std::uint64_t texture_source_signature(const psprecomp::GuestMemory &memory,
|
|
const TextureSetup &texture) noexcept {
|
|
if (texture.base == 0u || texture.width == 0u || texture.height == 0u) return 0u;
|
|
std::uint64_t bytes = 0u;
|
|
switch (texture.format) {
|
|
case 0u: case 1u: case 2u: case 6u:
|
|
bytes = static_cast<std::uint64_t>(texture.buffer_width) * texture.height * 2u; break;
|
|
case 3u: case 7u:
|
|
bytes = static_cast<std::uint64_t>(texture.buffer_width) * texture.height * 4u; break;
|
|
case 4u:
|
|
bytes = static_cast<std::uint64_t>((texture.buffer_width + 1u) >> 1u) * texture.height; break;
|
|
case 5u:
|
|
bytes = static_cast<std::uint64_t>(texture.buffer_width) * texture.height; break;
|
|
case 8u:
|
|
bytes = static_cast<std::uint64_t>((texture.buffer_width + 3u) >> 2u) *
|
|
((texture.height + 3u) >> 2u) * 8u; break;
|
|
case 9u: case 10u:
|
|
bytes = static_cast<std::uint64_t>((texture.buffer_width + 3u) >> 2u) *
|
|
((texture.height + 3u) >> 2u) * 16u; break;
|
|
default: return 0u;
|
|
}
|
|
if (texture.swizzled && texture.format <= 7u) {
|
|
// PSP swizzle stores 16-byte x 8-row blocks. Include storage padding so
|
|
// a write in a padded block still changes the signature.
|
|
std::uint64_t row = bytes / std::max<std::uint32_t>(1u, texture.height);
|
|
row = (row + 15u) & ~15ull;
|
|
bytes = row * ((static_cast<std::uint64_t>(texture.height) + 7u) & ~7ull);
|
|
}
|
|
if (bytes == 0u || bytes > std::numeric_limits<std::size_t>::max()) return 0u;
|
|
const auto size = static_cast<std::size_t>(bytes);
|
|
const std::uint8_t *pixels = memory.raw_pointer(texture.base, size);
|
|
if (pixels == nullptr) return 0u;
|
|
|
|
// Full hash for small dynamic surfaces; evenly distributed cache-line
|
|
// samples for large world textures. Stage 40 mixed one byte at a time, so a
|
|
// 1 KiB validation sample executed ~1,000 dependent multiply operations.
|
|
// Fold eight bytes per iteration instead. The signature is process-local
|
|
// cache validation, not a file format, so only collision quality matters.
|
|
std::uint64_t hash = 0x9E3779B97F4A7C15ull;
|
|
const auto mix64 = [&hash](std::uint64_t value) noexcept {
|
|
hash ^= value + 0x9E3779B97F4A7C15ull + (hash << 6u) + (hash >> 2u);
|
|
hash *= 0xD6E8FEB86659FD93ull;
|
|
hash ^= hash >> 29u;
|
|
};
|
|
const auto hash_range = [&](std::size_t begin, std::size_t end) noexcept {
|
|
std::size_t i = begin;
|
|
while (i + 8u <= end) {
|
|
std::uint64_t word{};
|
|
std::memcpy(&word, pixels + i, sizeof(word));
|
|
mix64(word);
|
|
i += 8u;
|
|
}
|
|
if (i < end) {
|
|
std::uint64_t tail = 0u;
|
|
std::memcpy(&tail, pixels + i, end - i);
|
|
mix64(tail ^ (static_cast<std::uint64_t>(end - i) << 56u));
|
|
}
|
|
};
|
|
if (size <= 4096u) {
|
|
hash_range(0u, size);
|
|
} else {
|
|
constexpr std::size_t blocks = 16u;
|
|
constexpr std::size_t block_bytes = 64u;
|
|
for (std::size_t block = 0u; block < blocks; ++block) {
|
|
const std::size_t center = (size - 1u) * block / (blocks - 1u);
|
|
const std::size_t begin = center > block_bytes / 2u ? center - block_bytes / 2u : 0u;
|
|
hash_range(begin, std::min(size, begin + block_bytes));
|
|
}
|
|
}
|
|
hash ^= static_cast<std::uint64_t>(size) +
|
|
(static_cast<std::uint64_t>(texture.width) << 32u) + texture.height;
|
|
hash *= 1099511628211ull;
|
|
return hash == 0u ? 1u : hash;
|
|
}
|
|
|
|
Color read_clut_fast(const psprecomp::GuestMemory &memory,
|
|
const TextureSetup &texture, std::uint32_t raw_index) {
|
|
const std::uint32_t index =
|
|
(((raw_index >> texture.clut_shift) & texture.clut_mask) |
|
|
(texture.clut_start & texture.clut_wrap_mask)) & texture.clut_wrap_mask;
|
|
const std::uint32_t offset = index * texture.clut_entry_bytes;
|
|
if (texture.clut_pixels != nullptr) {
|
|
const std::uint8_t *entry = texture.clut_pixels + offset;
|
|
if (texture.clut_format == 3u) {
|
|
return unpack32(static_cast<std::uint32_t>(entry[0]) |
|
|
(static_cast<std::uint32_t>(entry[1]) << 8u) |
|
|
(static_cast<std::uint32_t>(entry[2]) << 16u) |
|
|
(static_cast<std::uint32_t>(entry[3]) << 24u));
|
|
}
|
|
return unpack16(static_cast<std::uint16_t>(
|
|
static_cast<std::uint16_t>(entry[0]) |
|
|
(static_cast<std::uint16_t>(entry[1]) << 8u)),
|
|
texture.clut_format);
|
|
}
|
|
const std::uint32_t address = texture.clut_base + offset;
|
|
if (!memory.contains(address, texture.clut_entry_bytes)) return {};
|
|
if (texture.clut_format == 3u) return unpack32(memory.aot_load32(address));
|
|
return unpack16(memory.aot_load16(address), texture.clut_format);
|
|
}
|
|
|
|
|
|
std::uint16_t load_le16(const std::uint8_t *p) noexcept {
|
|
return static_cast<std::uint16_t>(static_cast<std::uint16_t>(p[0]) |
|
|
(static_cast<std::uint16_t>(p[1]) << 8u));
|
|
}
|
|
|
|
std::uint32_t load_le32(const std::uint8_t *p) noexcept {
|
|
return static_cast<std::uint32_t>(p[0]) |
|
|
(static_cast<std::uint32_t>(p[1]) << 8u) |
|
|
(static_cast<std::uint32_t>(p[2]) << 16u) |
|
|
(static_cast<std::uint32_t>(p[3]) << 24u);
|
|
}
|
|
|
|
Color dxt_color(std::uint16_t c1, std::uint16_t c2, std::uint32_t index,
|
|
std::uint8_t alpha, bool dxt1_alpha) noexcept {
|
|
const auto endpoint = [](std::uint16_t c, std::uint8_t a) noexcept {
|
|
// PSP S3TC endpoints use the GE's BGR565 bit ordering rather than
|
|
// PC DXT's usual RGB presentation.
|
|
return Color{
|
|
static_cast<std::uint8_t>((c >> 8u) & 0xF8u),
|
|
static_cast<std::uint8_t>((c >> 3u) & 0xFCu),
|
|
static_cast<std::uint8_t>((c << 3u) & 0xF8u),
|
|
a,
|
|
};
|
|
};
|
|
const Color a = endpoint(c1, alpha);
|
|
const Color b = endpoint(c2, alpha);
|
|
if (index == 0u) return a;
|
|
if (index == 1u) return b;
|
|
const auto mix23 = [](std::uint8_t first, std::uint8_t second) noexcept {
|
|
return static_cast<std::uint8_t>((static_cast<unsigned>(first) * 2u + second) / 3u);
|
|
};
|
|
if (c1 > c2 || !dxt1_alpha) {
|
|
if (index == 2u) return {mix23(a.r,b.r), mix23(a.g,b.g), mix23(a.b,b.b), alpha};
|
|
return {mix23(b.r,a.r), mix23(b.g,a.g), mix23(b.b,a.b), alpha};
|
|
}
|
|
if (index == 3u) return {0u,0u,0u,0u};
|
|
return {
|
|
static_cast<std::uint8_t>((static_cast<unsigned>(a.r) + b.r) / 2u),
|
|
static_cast<std::uint8_t>((static_cast<unsigned>(a.g) + b.g) / 2u),
|
|
static_cast<std::uint8_t>((static_cast<unsigned>(a.b) + b.b) / 2u),
|
|
alpha,
|
|
};
|
|
}
|
|
|
|
Color sample_dxt_texel(const psprecomp::GuestMemory &memory,
|
|
const TextureSetup &texture,
|
|
std::uint32_t x, std::uint32_t y) {
|
|
const std::uint32_t block_size = texture.format == 8u ? 8u : 16u;
|
|
const std::uint32_t blocks_per_row = std::max(1u, (texture.buffer_width + 3u) / 4u);
|
|
const std::uint32_t block_offset = ((y / 4u) * blocks_per_row + (x / 4u)) * block_size;
|
|
std::array<std::uint8_t,16> checked{};
|
|
const std::uint8_t *block = nullptr;
|
|
if (texture.pixels != nullptr) {
|
|
block = texture.pixels + block_offset;
|
|
} else {
|
|
if (!memory.contains(texture.base + block_offset, block_size)) return {};
|
|
for (std::uint32_t i=0;i<block_size;++i)
|
|
checked[i] = memory.aot_load8(texture.base + block_offset + i);
|
|
block = checked.data();
|
|
}
|
|
|
|
// PSP DXT blocks are byte-reversed relative to desktop S3TC: color row
|
|
// selectors come first, followed by the two 565 endpoints. DXT3/DXT5 then
|
|
// append alpha data after that color block.
|
|
const std::uint32_t row = y & 3u;
|
|
const std::uint32_t column = x & 3u;
|
|
const std::uint32_t color_index = (block[row] >> (column * 2u)) & 3u;
|
|
const std::uint16_t c1 = load_le16(block + 4u);
|
|
const std::uint16_t c2 = load_le16(block + 6u);
|
|
if (texture.format == 8u)
|
|
return dxt_color(c1,c2,color_index,255u,true);
|
|
if (texture.format == 9u) {
|
|
const std::uint16_t alpha_line = load_le16(block + 8u + row * 2u);
|
|
const std::uint8_t alpha = static_cast<std::uint8_t>(((alpha_line >> (column * 4u)) & 0xFu) * 17u);
|
|
return dxt_color(c1,c2,color_index,alpha,false);
|
|
}
|
|
|
|
const std::uint32_t alpha_data_low = load_le32(block + 8u);
|
|
const std::uint16_t alpha_data_high = load_le16(block + 12u);
|
|
const std::uint64_t alpha_bits = (static_cast<std::uint64_t>(alpha_data_high) << 32u) |
|
|
alpha_data_low;
|
|
const std::uint32_t alpha_index = static_cast<std::uint32_t>(
|
|
(alpha_bits >> (row * 12u + column * 3u)) & 7u);
|
|
const std::uint8_t alpha1 = block[14u];
|
|
const std::uint8_t alpha2 = block[15u];
|
|
std::array<std::uint8_t,8> palette{alpha1,alpha2,0u,0u,0u,0u,0u,255u};
|
|
if (alpha1 > alpha2) {
|
|
for (std::uint32_t i=1u;i<=6u;++i) {
|
|
const unsigned fixed = (static_cast<unsigned>(alpha1) * ((7u-i) << 8u)) / 7u +
|
|
(static_cast<unsigned>(alpha2) * (i << 8u)) / 7u;
|
|
palette[i+1u] = static_cast<std::uint8_t>((fixed + 31u) >> 8u);
|
|
}
|
|
} else {
|
|
for (std::uint32_t i=1u;i<=4u;++i) {
|
|
const unsigned fixed = (static_cast<unsigned>(alpha1) * ((5u-i) << 8u)) / 5u +
|
|
(static_cast<unsigned>(alpha2) * (i << 8u)) / 5u;
|
|
palette[i+1u] = static_cast<std::uint8_t>((fixed + 31u) >> 8u);
|
|
}
|
|
palette[6u]=0u; palette[7u]=255u;
|
|
}
|
|
return dxt_color(c1,c2,color_index,palette[alpha_index],false);
|
|
}
|
|
|
|
Color sample_texture_wrapped(const psprecomp::GuestMemory &memory,
|
|
const TextureSetup &texture,
|
|
std::uint32_t x, std::uint32_t y) {
|
|
const std::uint32_t format = texture.format;
|
|
const std::uint32_t buffer_width = texture.buffer_width;
|
|
const bool swizzled = texture.swizzled;
|
|
const std::uint32_t base = texture.base;
|
|
const std::uint8_t *const pixels = texture.pixels;
|
|
|
|
// Raw fetches when the whole texture resolved; otherwise the checked path.
|
|
const auto fetch8 = [&](std::uint32_t offset, std::uint8_t &out) {
|
|
if (pixels != nullptr) { out = pixels[offset]; return true; }
|
|
if (!memory.contains(base + offset, 1u)) return false;
|
|
out = memory.aot_load8(base + offset);
|
|
return true;
|
|
};
|
|
const auto fetch16 = [&](std::uint32_t offset, std::uint16_t &out) {
|
|
if (pixels != nullptr) {
|
|
out = static_cast<std::uint16_t>(static_cast<std::uint16_t>(pixels[offset]) |
|
|
(static_cast<std::uint16_t>(pixels[offset + 1u]) << 8u));
|
|
return true;
|
|
}
|
|
if (!memory.contains(base + offset, 2u)) return false;
|
|
out = memory.aot_load16(base + offset);
|
|
return true;
|
|
};
|
|
const auto fetch32 = [&](std::uint32_t offset, std::uint32_t &out) {
|
|
if (pixels != nullptr) {
|
|
out = static_cast<std::uint32_t>(pixels[offset]) |
|
|
(static_cast<std::uint32_t>(pixels[offset + 1u]) << 8u) |
|
|
(static_cast<std::uint32_t>(pixels[offset + 2u]) << 16u) |
|
|
(static_cast<std::uint32_t>(pixels[offset + 3u]) << 24u);
|
|
return true;
|
|
}
|
|
if (!memory.contains(base + offset, 4u)) return false;
|
|
out = memory.aot_load32(base + offset);
|
|
return true;
|
|
};
|
|
|
|
std::uint32_t offset{};
|
|
std::uint32_t raw_index{};
|
|
switch (format) {
|
|
case 0u: case 1u: case 2u: {
|
|
const std::uint32_t byte_x = x * 2u;
|
|
offset = swizzled ? swizzled_offset(byte_x, y, buffer_width * 2u) : (y * buffer_width * 2u + byte_x);
|
|
std::uint16_t texel{};
|
|
if (!fetch16(offset, texel)) return {};
|
|
return unpack16(texel, format);
|
|
}
|
|
case 3u: {
|
|
const std::uint32_t byte_x = x * 4u;
|
|
offset = swizzled ? swizzled_offset(byte_x, y, buffer_width * 4u) : (y * buffer_width * 4u + byte_x);
|
|
std::uint32_t texel{};
|
|
if (!fetch32(offset, texel)) return {};
|
|
return unpack32(texel);
|
|
}
|
|
case 4u: {
|
|
const std::uint32_t byte_x = x >> 1u;
|
|
offset = swizzled ? swizzled_offset(byte_x, y, (buffer_width + 1u) / 2u) : (y * ((buffer_width + 1u) / 2u) + byte_x);
|
|
std::uint8_t packed{};
|
|
if (!fetch8(offset, packed)) return {};
|
|
raw_index = (x & 1u) != 0u ? packed >> 4u : packed & 0xFu;
|
|
return read_clut_fast(memory, texture, raw_index);
|
|
}
|
|
case 5u: {
|
|
const std::uint32_t byte_x = x;
|
|
offset = swizzled ? swizzled_offset(byte_x, y, buffer_width) : (y * buffer_width + byte_x);
|
|
std::uint8_t packed{};
|
|
if (!fetch8(offset, packed)) return {};
|
|
return read_clut_fast(memory, texture, packed);
|
|
}
|
|
case 6u: {
|
|
const std::uint32_t byte_x = x * 2u;
|
|
offset = swizzled ? swizzled_offset(byte_x, y, buffer_width * 2u) : (y * buffer_width * 2u + byte_x);
|
|
std::uint16_t packed{};
|
|
if (!fetch16(offset, packed)) return {};
|
|
return read_clut_fast(memory, texture, packed);
|
|
}
|
|
case 7u: {
|
|
const std::uint32_t byte_x = x * 4u;
|
|
offset = swizzled ? swizzled_offset(byte_x, y, buffer_width * 4u) : (y * buffer_width * 4u + byte_x);
|
|
std::uint32_t packed{};
|
|
if (!fetch32(offset, packed)) return {};
|
|
return read_clut_fast(memory, texture, packed);
|
|
}
|
|
case 8u: case 9u: case 10u:
|
|
return sample_dxt_texel(memory, texture, x, y);
|
|
default:
|
|
return {};
|
|
}
|
|
}
|
|
|
|
Color sample_texture_nearest(const psprecomp::GuestMemory &memory,
|
|
const TextureSetup &texture, float u, float v) {
|
|
const std::uint32_t x = wrap_coord(floor_to_int(u), texture.width, texture.clamp_u);
|
|
const std::uint32_t y = wrap_coord(floor_to_int(v), texture.height, texture.clamp_v);
|
|
return sample_texture_wrapped(memory, texture, x, y);
|
|
}
|
|
|
|
Color lerp_color(Color a, Color b, float t) noexcept {
|
|
#if PSPRECOMP_GE_X86_SIMD
|
|
const __m128 av = _mm_set_ps(static_cast<float>(a.a), static_cast<float>(a.b),
|
|
static_cast<float>(a.g), static_cast<float>(a.r));
|
|
const __m128 bv = _mm_set_ps(static_cast<float>(b.a), static_cast<float>(b.b),
|
|
static_cast<float>(b.g), static_cast<float>(b.r));
|
|
const __m128 result = _mm_add_ps(av, _mm_mul_ps(_mm_sub_ps(bv, av), _mm_set1_ps(t)));
|
|
// Every interpolated lane stays in [0, 255]. For non-negative values,
|
|
// trunc(x + 0.5) is exactly C lroundf's halfway-away-from-zero rule. Pack
|
|
// the four lanes directly instead of spilling and calling lroundf four
|
|
// times (twelve calls for one bilinear texture sample).
|
|
const __m128i rounded = _mm_cvttps_epi32(
|
|
_mm_add_ps(result, _mm_set1_ps(0.5f)));
|
|
const __m128i packed16 = _mm_packs_epi32(rounded, _mm_setzero_si128());
|
|
const __m128i packed8 = _mm_packus_epi16(packed16, _mm_setzero_si128());
|
|
const std::uint32_t packed = static_cast<std::uint32_t>(_mm_cvtsi128_si32(packed8));
|
|
return std::bit_cast<Color>(packed);
|
|
#else
|
|
auto channel = [t](std::uint8_t x, std::uint8_t y) {
|
|
return round_clamp_to_byte(static_cast<float>(x) + (static_cast<float>(y) - x) * t);
|
|
};
|
|
return {channel(a.r, b.r), channel(a.g, b.g), channel(a.b, b.b), channel(a.a, b.a)};
|
|
#endif
|
|
}
|
|
|
|
Color sample_texture(const psprecomp::GuestMemory &memory,
|
|
const TextureSetup &texture, float u, float v) {
|
|
if (!texture.linear) return sample_texture_nearest(memory, texture, u, v);
|
|
const float shifted_u = u - 0.5f;
|
|
const float shifted_v = v - 0.5f;
|
|
const std::int32_t bx = floor_to_int(shifted_u);
|
|
const std::int32_t by = floor_to_int(shifted_v);
|
|
const float fx = shifted_u - static_cast<float>(bx);
|
|
const float fy = shifted_v - static_cast<float>(by);
|
|
const std::uint32_t x0 = wrap_coord(bx, texture.width, texture.clamp_u);
|
|
const std::uint32_t x1 = wrap_coord(bx + 1, texture.width, texture.clamp_u);
|
|
const std::uint32_t y0 = wrap_coord(by, texture.height, texture.clamp_v);
|
|
const std::uint32_t y1 = wrap_coord(by + 1, texture.height, texture.clamp_v);
|
|
|
|
// More than 94% of VCS textured primitives use bilinear, swizzled T4
|
|
// textures. Decode that single dominant layout once per fragment instead
|
|
// of dispatching the generic format/swizzle switch four times. Adjacent
|
|
// nibbles share their packed byte when possible.
|
|
if (texture.format == 4u && texture.swizzled && texture.pixels != nullptr &&
|
|
texture.clut_pixels != nullptr) {
|
|
const std::uint32_t row_bytes = (texture.buffer_width + 1u) >> 1u;
|
|
const std::uint32_t blocks_per_row = (row_bytes + 15u) >> 4u;
|
|
const auto packed_offset = [blocks_per_row](std::uint32_t x, std::uint32_t y) noexcept {
|
|
const std::uint32_t byte_x = x >> 1u;
|
|
return (((y >> 3u) * blocks_per_row + (byte_x >> 4u)) << 7u) +
|
|
((y & 7u) << 4u) + (byte_x & 15u);
|
|
};
|
|
const auto index_from = [](std::uint8_t packed, std::uint32_t x) noexcept {
|
|
return static_cast<std::uint32_t>((x & 1u) != 0u ? packed >> 4u : packed & 0xFu);
|
|
};
|
|
|
|
const std::uint32_t o00 = packed_offset(x0, y0);
|
|
const std::uint32_t o10 = packed_offset(x1, y0);
|
|
const std::uint32_t o01 = packed_offset(x0, y1);
|
|
const std::uint32_t o11 = packed_offset(x1, y1);
|
|
const std::uint8_t p00 = texture.pixels[o00];
|
|
const std::uint8_t p10 = o10 == o00 ? p00 : texture.pixels[o10];
|
|
const std::uint8_t p01 = texture.pixels[o01];
|
|
const std::uint8_t p11 = o11 == o01 ? p01 : texture.pixels[o11];
|
|
const Color c00 = read_clut_fast(memory, texture, index_from(p00, x0));
|
|
const Color c10 = read_clut_fast(memory, texture, index_from(p10, x1));
|
|
const Color c01 = read_clut_fast(memory, texture, index_from(p01, x0));
|
|
const Color c11 = read_clut_fast(memory, texture, index_from(p11, x1));
|
|
return lerp_color(lerp_color(c00, c10, fx), lerp_color(c01, c11, fx), fy);
|
|
}
|
|
|
|
const Color c00 = sample_texture_wrapped(memory, texture, x0, y0);
|
|
const Color c10 = sample_texture_wrapped(memory, texture, x1, y0);
|
|
const Color c01 = sample_texture_wrapped(memory, texture, x0, y1);
|
|
const Color c11 = sample_texture_wrapped(memory, texture, x1, y1);
|
|
return lerp_color(lerp_color(c00, c10, fx), lerp_color(c01, c11, fx), fy);
|
|
}
|
|
|
|
|
|
bool decode_texture_rgba_into(const psprecomp::GuestMemory &memory,
|
|
const TextureSetup &texture,
|
|
std::span<std::byte> rgba8) {
|
|
if (texture.format > 10u || texture.width == 0u || texture.height == 0u ||
|
|
texture.width > 2048u || texture.height > 2048u) return false;
|
|
const std::uint64_t byte_count = static_cast<std::uint64_t>(texture.width) *
|
|
texture.height * 4ull;
|
|
if (byte_count != rgba8.size()) return false;
|
|
|
|
// Resolve the 16-entry T4 palette once. VCS overwhelmingly uses swizzled
|
|
// T4 world textures; the old generic decoder performed a format switch,
|
|
// swizzle calculation and CLUT transform for every texel. One packed byte
|
|
// contains two pixels, so this path also halves source loads.
|
|
std::array<Color, 16> t4_palette{};
|
|
std::array<std::uint32_t, 16> t4_palette_rgba{};
|
|
const bool fast_t4 = texture.format == 4u && texture.pixels != nullptr;
|
|
if (fast_t4) {
|
|
for (std::uint32_t i = 0; i < t4_palette.size(); ++i) {
|
|
t4_palette[i] = read_clut_fast(memory, texture, i);
|
|
const Color c = t4_palette[i];
|
|
std::uint32_t packed = static_cast<std::uint32_t>(c.r) |
|
|
(static_cast<std::uint32_t>(c.g) << 8u) |
|
|
(static_cast<std::uint32_t>(c.b) << 16u) |
|
|
(static_cast<std::uint32_t>(c.a) << 24u);
|
|
if constexpr (std::endian::native == std::endian::big)
|
|
packed = ((packed & 0x000000FFu) << 24u) | ((packed & 0x0000FF00u) << 8u) |
|
|
((packed & 0x00FF0000u) >> 8u) | ((packed & 0xFF000000u) >> 24u);
|
|
t4_palette_rgba[i] = packed;
|
|
}
|
|
}
|
|
|
|
auto decode_rows = [&](std::uint32_t first_row, std::uint32_t last_row) {
|
|
if (fast_t4) {
|
|
const std::uint32_t row_bytes = (texture.buffer_width + 1u) >> 1u;
|
|
const std::uint32_t blocks_per_row = (row_bytes + 15u) >> 4u;
|
|
for (std::uint32_t y = first_row; y <= last_row; ++y) {
|
|
std::byte *dst = rgba8.data() +
|
|
static_cast<std::size_t>(y) * texture.width * 4u;
|
|
const auto source_offset = [&](std::uint32_t byte_x) noexcept {
|
|
if (!texture.swizzled)
|
|
return static_cast<std::size_t>(y) * row_bytes + byte_x;
|
|
return static_cast<std::size_t>(
|
|
(((y >> 3u) * blocks_per_row + (byte_x >> 4u)) << 7u) +
|
|
((y & 7u) << 4u) + (byte_x & 15u));
|
|
};
|
|
for (std::uint32_t x = 0u; x < texture.width; x += 2u) {
|
|
const std::uint8_t packed = texture.pixels[source_offset(x >> 1u)];
|
|
const std::uint32_t lo = t4_palette_rgba[packed & 0x0Fu];
|
|
std::memcpy(dst, &lo, sizeof(lo));
|
|
dst += 4;
|
|
if (x + 1u < texture.width) {
|
|
const std::uint32_t hi = t4_palette_rgba[packed >> 4u];
|
|
std::memcpy(dst, &hi, sizeof(hi));
|
|
dst += 4;
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
std::size_t offset = static_cast<std::size_t>(first_row) * texture.width * 4u;
|
|
for (std::uint32_t y = first_row; y <= last_row; ++y) {
|
|
for (std::uint32_t x = 0u; x < texture.width; ++x) {
|
|
const Color color = sample_texture_wrapped(memory, texture, x, y);
|
|
rgba8[offset + 0u] = static_cast<std::byte>(color.r);
|
|
rgba8[offset + 1u] = static_cast<std::byte>(color.g);
|
|
rgba8[offset + 2u] = static_cast<std::byte>(color.b);
|
|
rgba8[offset + 3u] = static_cast<std::byte>(color.a);
|
|
offset += 4u;
|
|
}
|
|
}
|
|
};
|
|
|
|
static const bool parallel_texture_decode = [] {
|
|
const char *text = std::getenv("PSPRECOMP_GE_PARALLEL_TEXTURE_DECODE");
|
|
return text == nullptr || (*text != '\0' && std::strcmp(text, "0") != 0);
|
|
}();
|
|
RowWorkerPool &pool = RowWorkerPool::instance();
|
|
// T4 now does much less work per texel, so fan-out is worthwhile at a
|
|
// slightly larger size. Other formats retain the old crossover.
|
|
const std::uint64_t texels = static_cast<std::uint64_t>(texture.width) * texture.height;
|
|
const std::uint64_t threshold = fast_t4 ? 32768u : 16384u;
|
|
if (!parallel_texture_decode || pool.worker_count() <= 1u ||
|
|
texels < threshold || texture.height < 2u) {
|
|
decode_rows(0u, texture.height - 1u);
|
|
} else {
|
|
// Keep a meaningful amount of pixel work behind each wake-up. The old
|
|
// path could wake 32 threads for a 128-row T4 texture, spending more on
|
|
// synchronization than on the now-specialized nibble decoder.
|
|
const std::uint64_t texels_per_participant = fast_t4 ? 8192u : 4096u;
|
|
const unsigned useful_participants = std::max(2u, std::min<unsigned>(
|
|
pool.worker_count(), static_cast<unsigned>((texels + texels_per_participant - 1u) /
|
|
texels_per_participant)));
|
|
pool.run(0, static_cast<std::int32_t>(texture.height) - 1,
|
|
[&decode_rows](unsigned, std::int32_t begin, std::int32_t end) {
|
|
decode_rows(static_cast<std::uint32_t>(begin),
|
|
static_cast<std::uint32_t>(end));
|
|
}, useful_participants);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool decode_texture_rgba(const psprecomp::GuestMemory &memory,
|
|
const TextureSetup &texture,
|
|
std::vector<std::byte> &rgba8) {
|
|
const std::uint64_t byte_count = static_cast<std::uint64_t>(texture.width) *
|
|
texture.height * 4ull;
|
|
if (byte_count > static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max()))
|
|
return false;
|
|
try {
|
|
rgba8.resize(static_cast<std::size_t>(byte_count));
|
|
if (decode_texture_rgba_into(memory, texture, rgba8)) return true;
|
|
} catch (...) {
|
|
}
|
|
rgba8.clear();
|
|
return false;
|
|
}
|
|
|
|
// Per-primitive raster state.
|
|
//
|
|
// write_fragment used to re-decode every one of these GE registers for each
|
|
// pixel it touched -- framebuffer format/stride/address, both scissor corners,
|
|
// the clear flags, the colour write mask and the whole depth configuration.
|
|
// At 480x272 with heavy overdraw that decoding dominated the frame. The values
|
|
// cannot change inside a primitive, so they are decoded once here and the
|
|
// pixel loop only reads plain fields.
|
|
struct FragmentSetup {
|
|
std::uint32_t framebuffer_format{};
|
|
std::uint32_t framebuffer_stride{};
|
|
std::uint32_t framebuffer_base{};
|
|
std::uint32_t framebuffer_bpp{};
|
|
std::int32_t scissor_x0{};
|
|
std::int32_t scissor_y0{};
|
|
std::int32_t scissor_x1{};
|
|
std::int32_t scissor_y1{};
|
|
std::uint32_t write_mask{};
|
|
std::uint32_t depth_stride{};
|
|
std::uint32_t depth_base{};
|
|
std::uint32_t depth_function{};
|
|
bool texture_enabled{};
|
|
bool clear_mode{};
|
|
bool clear_color{};
|
|
bool clear_alpha{};
|
|
bool clear_depth{};
|
|
bool depth_test_enabled{};
|
|
bool depth_write_enabled{};
|
|
bool valid{};
|
|
// Resolved once per primitive; null falls back to the checked accessors.
|
|
std::uint8_t *color_pixels{};
|
|
std::uint8_t *depth_pixels{};
|
|
TextureSetup texture{};
|
|
// Blend, alpha-test and texture-function registers, decoded once.
|
|
bool blend_enabled{};
|
|
std::uint32_t blend_equation{};
|
|
std::uint32_t blend_source_factor{};
|
|
std::uint32_t blend_dest_factor{};
|
|
Color blend_fix_source{};
|
|
Color blend_fix_dest{};
|
|
bool alpha_test_enabled{};
|
|
std::uint32_t alpha_function{};
|
|
std::uint32_t alpha_mask{};
|
|
std::uint32_t alpha_reference{};
|
|
std::uint32_t texture_function{};
|
|
Color texture_env{};
|
|
bool texture_use_alpha{};
|
|
bool texture_double_color{};
|
|
};
|
|
|
|
|
|
// Resolves the frame and depth buffers to raw pointers for the rows this
|
|
// primitive can touch. Both stay null when the range is not contiguous in host
|
|
// memory, and every pixel then takes the original checked path.
|
|
void bind_fragment_buffers(FragmentSetup &setup, psprecomp::GuestMemory &memory,
|
|
const std::array<std::uint32_t, 256> &commands) noexcept {
|
|
if (!setup.valid || setup.scissor_y1 < 0) return;
|
|
if (setup.texture_enabled) setup.texture = make_texture_setup(memory, commands);
|
|
const std::size_t rows = static_cast<std::size_t>(setup.scissor_y1) + 1u;
|
|
const std::size_t color_bytes = rows * setup.framebuffer_stride * setup.framebuffer_bpp;
|
|
setup.color_pixels = memory.raw_pointer(setup.framebuffer_base, color_bytes);
|
|
if (setup.depth_stride != 0u) {
|
|
const std::size_t depth_bytes = rows * setup.depth_stride * 2u;
|
|
setup.depth_pixels = memory.raw_pointer(setup.depth_base, depth_bytes);
|
|
}
|
|
}
|
|
|
|
FragmentSetup make_fragment_setup(const std::array<std::uint32_t, 256> &commands) noexcept {
|
|
FragmentSetup setup{};
|
|
setup.framebuffer_stride = data24(commands[0x9Du]) & 0x7FCu;
|
|
if (setup.framebuffer_stride == 0u) return setup;
|
|
setup.framebuffer_format = data24(commands[0xD2u]) & 3u;
|
|
setup.framebuffer_base = framebuffer_address(commands);
|
|
setup.framebuffer_bpp = bytes_per_pixel(setup.framebuffer_format);
|
|
|
|
const std::uint32_t scissor1 = data24(commands[0xD4u]);
|
|
const std::uint32_t scissor2 = data24(commands[0xD5u]);
|
|
setup.scissor_x0 = static_cast<std::int32_t>(scissor1 & 0x3FFu);
|
|
setup.scissor_y0 = static_cast<std::int32_t>((scissor1 >> 10u) & 0x3FFu);
|
|
setup.scissor_x1 = scissor2 == 0u ? static_cast<std::int32_t>(setup.framebuffer_stride - 1u)
|
|
: static_cast<std::int32_t>(scissor2 & 0x3FFu);
|
|
setup.scissor_y1 = scissor2 == 0u ? 271 : static_cast<std::int32_t>((scissor2 >> 10u) & 0x3FFu);
|
|
|
|
const std::uint32_t clear = data24(commands[0xD3u]);
|
|
setup.clear_mode = (clear & 1u) != 0u;
|
|
setup.clear_color = (clear & 0x100u) != 0u;
|
|
setup.clear_alpha = (clear & 0x200u) != 0u;
|
|
setup.clear_depth = (clear & 0x400u) != 0u;
|
|
setup.texture_enabled = (data24(commands[0x1Eu]) & 1u) != 0u;
|
|
setup.write_mask = data24(commands[0xE8u]) | ((data24(commands[0xE9u]) & 0xFFu) << 24u);
|
|
|
|
setup.depth_stride = data24(commands[0x9Fu]) & 0x7FCu;
|
|
setup.depth_base = depthbuffer_address(commands);
|
|
setup.depth_test_enabled = (data24(commands[0x23u]) & 1u) != 0u;
|
|
setup.depth_function = data24(commands[0xDEu]) & 7u;
|
|
setup.depth_write_enabled = (data24(commands[0xE7u]) & 1u) == 0u;
|
|
const std::uint32_t blend_mode = data24(commands[0xDFu]);
|
|
setup.blend_enabled = (data24(commands[0x21u]) & 1u) != 0u;
|
|
setup.blend_equation = (blend_mode >> 8u) & 7u;
|
|
setup.blend_source_factor = blend_mode & 0xFu;
|
|
setup.blend_dest_factor = (blend_mode >> 4u) & 0xFu;
|
|
setup.blend_fix_source = unpack32(data24(commands[0xE0u]) | 0xFF000000u);
|
|
setup.blend_fix_dest = unpack32(data24(commands[0xE1u]) | 0xFF000000u);
|
|
|
|
const std::uint32_t alpha = data24(commands[0xDBu]);
|
|
setup.alpha_test_enabled = (data24(commands[0x22u]) & 1u) != 0u;
|
|
setup.alpha_function = alpha & 7u;
|
|
setup.alpha_mask = (alpha >> 16u) & 0xFFu;
|
|
setup.alpha_reference = (alpha >> 8u) & 0xFFu;
|
|
|
|
const std::uint32_t texfunc = data24(commands[0xC9u]);
|
|
setup.texture_function = texfunc & 7u;
|
|
setup.texture_use_alpha = (texfunc & 0x100u) != 0u;
|
|
setup.texture_double_color = (texfunc & 0x10000u) != 0u;
|
|
setup.texture_env = unpack32(data24(commands[0xCAu]) | 0xFF000000u);
|
|
|
|
setup.valid = true;
|
|
return setup;
|
|
}
|
|
|
|
FragmentSetup make_fragment_setup_cached(const std::array<std::uint32_t, 256> &commands) noexcept {
|
|
// Only registers consumed by make_fragment_setup participate. Most city
|
|
// draws repeat this state for long runs, so avoid decoding it again until a
|
|
// relevant register actually changes.
|
|
constexpr std::array<std::uint8_t, 23> regs{{
|
|
0x9D,0xD2,0x9C,0xD4,0xD5,0xD3,0x1E,0xE8,0xE9,0x9F,0x9E,0x23,
|
|
0xDE,0xE7,0xDF,0x21,0xE0,0xE1,0xDB,0x22,0xC9,0xCA,0x00}};
|
|
struct Cache {
|
|
std::array<std::uint32_t, regs.size()> values{};
|
|
FragmentSetup setup{};
|
|
bool valid{};
|
|
};
|
|
static thread_local Cache cache;
|
|
std::array<std::uint32_t, regs.size()> values{};
|
|
for (std::size_t i = 0; i + 1 < regs.size(); ++i) values[i] = commands[regs[i]];
|
|
// Framebuffer/depth base helpers also consume the high-address words.
|
|
values.back() = commands[0x9C] ^ (commands[0x9E] * 0x9E3779B9u);
|
|
if (cache.valid && cache.values == values) return cache.setup;
|
|
cache.values = values;
|
|
cache.setup = make_fragment_setup(commands);
|
|
cache.valid = true;
|
|
return cache.setup;
|
|
}
|
|
|
|
std::uint8_t mul8(std::uint8_t a, std::uint8_t b) noexcept {
|
|
return static_cast<std::uint8_t>((static_cast<std::uint32_t>(a) * b + 127u) / 255u);
|
|
}
|
|
|
|
Color apply_texture_function(Color vertex, Color texture, const FragmentSetup &setup) noexcept {
|
|
const std::uint32_t function = setup.texture_function;
|
|
const bool use_alpha = setup.texture_use_alpha;
|
|
const bool double_color = setup.texture_double_color;
|
|
Color out{};
|
|
switch (function) {
|
|
case 0u: // MODULATE
|
|
out = {mul8(vertex.r, texture.r), mul8(vertex.g, texture.g), mul8(vertex.b, texture.b),
|
|
use_alpha ? mul8(vertex.a, texture.a) : vertex.a};
|
|
break;
|
|
case 1u: { // DECAL
|
|
const std::uint32_t alpha = use_alpha ? texture.a : 255u;
|
|
auto decal = [alpha](std::uint8_t vc, std::uint8_t tc) {
|
|
return static_cast<std::uint8_t>((static_cast<std::uint32_t>(tc) * alpha +
|
|
static_cast<std::uint32_t>(vc) * (255u - alpha) + 127u) / 255u);
|
|
};
|
|
out = {decal(vertex.r, texture.r), decal(vertex.g, texture.g), decal(vertex.b, texture.b), vertex.a};
|
|
break;
|
|
}
|
|
case 2u: { // BLEND
|
|
const Color env = setup.texture_env;
|
|
auto blend = [](std::uint8_t vc, std::uint8_t tc, std::uint8_t ec) {
|
|
return static_cast<std::uint8_t>((static_cast<std::uint32_t>(vc) * (255u - tc) +
|
|
static_cast<std::uint32_t>(ec) * tc + 127u) / 255u);
|
|
};
|
|
out = {blend(vertex.r, texture.r, env.r), blend(vertex.g, texture.g, env.g),
|
|
blend(vertex.b, texture.b, env.b), use_alpha ? mul8(vertex.a, texture.a) : vertex.a};
|
|
break;
|
|
}
|
|
case 3u: // REPLACE
|
|
out = texture;
|
|
if (!use_alpha) out.a = vertex.a;
|
|
break;
|
|
case 4u: // ADD
|
|
out = {static_cast<std::uint8_t>(std::min(255u, static_cast<unsigned>(vertex.r) + texture.r)),
|
|
static_cast<std::uint8_t>(std::min(255u, static_cast<unsigned>(vertex.g) + texture.g)),
|
|
static_cast<std::uint8_t>(std::min(255u, static_cast<unsigned>(vertex.b) + texture.b)),
|
|
use_alpha ? mul8(vertex.a, texture.a) : vertex.a};
|
|
break;
|
|
default:
|
|
out = texture;
|
|
break;
|
|
}
|
|
if (double_color) {
|
|
out.r = static_cast<std::uint8_t>(std::min(255u, static_cast<unsigned>(out.r) * 2u));
|
|
out.g = static_cast<std::uint8_t>(std::min(255u, static_cast<unsigned>(out.g) * 2u));
|
|
out.b = static_cast<std::uint8_t>(std::min(255u, static_cast<unsigned>(out.b) * 2u));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
bool compare_value(std::uint32_t function, std::uint32_t left, std::uint32_t right) noexcept {
|
|
switch (function & 7u) {
|
|
case 0u: return false;
|
|
case 1u: return true;
|
|
case 2u: return left == right;
|
|
case 3u: return left != right;
|
|
case 4u: return left < right;
|
|
case 5u: return left <= right;
|
|
case 6u: return left > right;
|
|
case 7u: return left >= right;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool alpha_test(Color source, const FragmentSetup &setup) noexcept {
|
|
if (!setup.alpha_test_enabled) return true;
|
|
return compare_value(setup.alpha_function, source.a & setup.alpha_mask,
|
|
setup.alpha_reference & setup.alpha_mask);
|
|
}
|
|
|
|
std::array<float, 4> color_float(Color c) noexcept {
|
|
return {c.r / 255.0f, c.g / 255.0f, c.b / 255.0f, c.a / 255.0f};
|
|
}
|
|
|
|
std::array<float, 4> blend_factor(std::uint32_t factor, bool source_factor,
|
|
const std::array<float, 4> &src,
|
|
const std::array<float, 4> &dst,
|
|
Color fixed) noexcept {
|
|
switch (factor) {
|
|
case 0u: return source_factor ? std::array<float,4>{dst[0], dst[1], dst[2], dst[3]}
|
|
: std::array<float,4>{src[0], src[1], src[2], src[3]};
|
|
case 1u: return source_factor ? std::array<float,4>{1-dst[0],1-dst[1],1-dst[2],1-dst[3]}
|
|
: std::array<float,4>{1-src[0],1-src[1],1-src[2],1-src[3]};
|
|
case 2u: return {src[3], src[3], src[3], src[3]};
|
|
case 3u: return {1-src[3],1-src[3],1-src[3],1-src[3]};
|
|
case 4u: return {dst[3],dst[3],dst[3],dst[3]};
|
|
case 5u: return {1-dst[3],1-dst[3],1-dst[3],1-dst[3]};
|
|
case 6u: return {std::min(1.0f, 2*src[3]),std::min(1.0f, 2*src[3]),std::min(1.0f, 2*src[3]),std::min(1.0f, 2*src[3])};
|
|
case 7u: return {std::min(1.0f, 2*(1-src[3])),std::min(1.0f, 2*(1-src[3])),std::min(1.0f, 2*(1-src[3])),std::min(1.0f, 2*(1-src[3]))};
|
|
case 8u: return {std::min(1.0f, 2*dst[3]),std::min(1.0f, 2*dst[3]),std::min(1.0f, 2*dst[3]),std::min(1.0f, 2*dst[3])};
|
|
case 9u: return {std::min(1.0f, 2*(1-dst[3])),std::min(1.0f, 2*(1-dst[3])),std::min(1.0f, 2*(1-dst[3])),std::min(1.0f, 2*(1-dst[3]))};
|
|
case 10u: return color_float(fixed);
|
|
default: return {1,1,1,1};
|
|
}
|
|
}
|
|
|
|
Color blend_pixel(Color source, Color destination, const FragmentSetup &setup) noexcept {
|
|
if (!setup.blend_enabled) return source;
|
|
|
|
// VCS overwhelmingly uses the standard source-alpha blend. The generic
|
|
// path below builds six float arrays and dispatches two factor switches per
|
|
// fragment. For equation ADD, SRC_ALPHA, ONE_MINUS_SRC_ALPHA the exact
|
|
// positive-domain float result rounds to the same byte as this integer
|
|
// expression; denominator 255 is odd, so an exact .5 tie cannot occur.
|
|
if (setup.blend_equation == 0u && setup.blend_source_factor == 2u &&
|
|
setup.blend_dest_factor == 3u) {
|
|
const std::uint32_t alpha = source.a;
|
|
const std::uint32_t inverse = 255u - alpha;
|
|
const auto channel = [alpha, inverse](std::uint8_t src, std::uint8_t dst) noexcept {
|
|
return static_cast<std::uint8_t>((static_cast<std::uint32_t>(src) * alpha +
|
|
static_cast<std::uint32_t>(dst) * inverse + 127u) / 255u);
|
|
};
|
|
return {channel(source.r, destination.r), channel(source.g, destination.g),
|
|
channel(source.b, destination.b), channel(source.a, destination.a)};
|
|
}
|
|
const std::uint32_t equation = setup.blend_equation;
|
|
const auto src = color_float(source);
|
|
const auto dst = color_float(destination);
|
|
const auto fa = blend_factor(setup.blend_source_factor, true, src, dst, setup.blend_fix_source);
|
|
const auto fb = blend_factor(setup.blend_dest_factor, false, src, dst, setup.blend_fix_dest);
|
|
std::array<float,4> out{};
|
|
for (std::size_t i = 0; i < 4u; ++i) {
|
|
const float a = src[i] * fa[i];
|
|
const float b = dst[i] * fb[i];
|
|
switch (equation) {
|
|
case 0u: out[i] = a + b; break;
|
|
case 1u: out[i] = a - b; break;
|
|
case 2u: out[i] = b - a; break;
|
|
case 3u: out[i] = std::min(src[i], dst[i]); break;
|
|
case 4u: out[i] = std::max(src[i], dst[i]); break;
|
|
case 5u: out[i] = std::fabs(src[i] - dst[i]); break;
|
|
default: out[i] = a + b; break;
|
|
}
|
|
}
|
|
auto to8 = [](float v) { return round_clamp_to_byte(v * 255.0f); };
|
|
return {to8(out[0]),to8(out[1]),to8(out[2]),to8(out[3])};
|
|
}
|
|
|
|
bool depth_test_and_write(psprecomp::GuestMemory &memory, const FragmentSetup &setup,
|
|
std::int32_t x, std::int32_t y, std::uint16_t z,
|
|
bool clear_depth) {
|
|
if (setup.depth_stride == 0u) return true;
|
|
const std::size_t index =
|
|
static_cast<std::size_t>(y) * setup.depth_stride + static_cast<std::size_t>(x);
|
|
if (setup.depth_pixels != nullptr) {
|
|
std::uint8_t *pixel = setup.depth_pixels + index * 2u;
|
|
if (clear_depth) {
|
|
pixel[0] = static_cast<std::uint8_t>(z);
|
|
pixel[1] = static_cast<std::uint8_t>(z >> 8u);
|
|
return true;
|
|
}
|
|
bool passed = true;
|
|
if (setup.depth_test_enabled) {
|
|
const std::uint16_t stored = static_cast<std::uint16_t>(
|
|
static_cast<std::uint16_t>(pixel[0]) |
|
|
(static_cast<std::uint16_t>(pixel[1]) << 8u));
|
|
passed = compare_value(setup.depth_function, z, stored);
|
|
}
|
|
if (passed && setup.depth_write_enabled) {
|
|
pixel[0] = static_cast<std::uint8_t>(z);
|
|
pixel[1] = static_cast<std::uint8_t>(z >> 8u);
|
|
}
|
|
return passed;
|
|
}
|
|
|
|
const std::uint32_t address = setup.depth_base + static_cast<std::uint32_t>(index) * 2u;
|
|
if (!memory.contains(address, 2u)) return true;
|
|
if (clear_depth) {
|
|
memory.aot_store16(address, z);
|
|
return true;
|
|
}
|
|
bool passed = true;
|
|
if (setup.depth_test_enabled) {
|
|
passed = compare_value(setup.depth_function, z, memory.aot_load16(address));
|
|
}
|
|
if (passed && setup.depth_write_enabled) memory.aot_store16(address, z);
|
|
return passed;
|
|
}
|
|
|
|
void rasterize_rectangle(psprecomp::GuestMemory &memory,
|
|
const std::array<std::uint32_t, 256> &commands,
|
|
const FragmentSetup &setup,
|
|
const Vertex &a, const Vertex &b, GeRenderStats &stats) {
|
|
record_screen_vertex(stats, a);
|
|
record_screen_vertex(stats, b);
|
|
if (!setup.valid) return;
|
|
// The triangle path has skipped GPU-owned surfaces since Stage 35; this one
|
|
// never did, so every sceGuClear and every full-screen blit into the surface
|
|
// the swapchain shows was still being filled on the CPU as well.
|
|
if (software_raster_skipped(commands)) return;
|
|
const std::uint32_t framebuffer_format = setup.framebuffer_format;
|
|
const std::uint32_t framebuffer_stride = setup.framebuffer_stride;
|
|
const std::uint32_t fb = setup.framebuffer_base;
|
|
const std::uint32_t bpp = setup.framebuffer_bpp;
|
|
|
|
const float min_x_f = std::min(a.x, b.x);
|
|
const float max_x_f = std::max(a.x, b.x);
|
|
const float min_y_f = std::min(a.y, b.y);
|
|
const float max_y_f = std::max(a.y, b.y);
|
|
std::int32_t x0 = static_cast<std::int32_t>(std::ceil(min_x_f));
|
|
std::int32_t x1 = static_cast<std::int32_t>(std::ceil(max_x_f)) - 1;
|
|
std::int32_t y0 = static_cast<std::int32_t>(std::ceil(min_y_f));
|
|
std::int32_t y1 = static_cast<std::int32_t>(std::ceil(max_y_f)) - 1;
|
|
|
|
x0 = std::max(x0, setup.scissor_x0); y0 = std::max(y0, setup.scissor_y0);
|
|
x1 = std::min(x1, setup.scissor_x1); y1 = std::min(y1, setup.scissor_y1);
|
|
if (x0 > x1 || y0 > y1) return;
|
|
|
|
const float dx = b.x - a.x;
|
|
const float dy = b.y - a.y;
|
|
const bool clear_mode = setup.clear_mode;
|
|
const bool clear_color = setup.clear_color;
|
|
const bool clear_alpha = setup.clear_alpha;
|
|
const bool clear_depth = setup.clear_depth;
|
|
const bool shaded_texture = setup.texture_enabled && !clear_mode;
|
|
const std::uint32_t write_mask = setup.write_mask;
|
|
const std::int32_t rectangle_width = x1 - x0 + 1;
|
|
const std::int64_t covered = static_cast<std::int64_t>(rectangle_width) *
|
|
static_cast<std::int64_t>(y1 - y0 + 1);
|
|
const bool phase_diag = ge_phase_diag_enabled();
|
|
RowWorkerPool &pool = RowWorkerPool::instance();
|
|
|
|
// PSP games commonly clear the entire color/depth surface with one GE
|
|
// rectangle. The generic fragment path was reading, unpacking, blending and
|
|
// repacking every pixel even when all channels and a constant Z were being
|
|
// overwritten. Bulk row fills preserve the exact packed result and turn a
|
|
// 130,560-fragment clear into a handful of vectorized memory operations.
|
|
const bool constant_depth = a.z == b.z;
|
|
const bool full_color_clear = clear_color &&
|
|
(clear_alpha || framebuffer_format == 0u);
|
|
const bool no_color_clear = !clear_color && !clear_alpha;
|
|
const bool fast_depth_clear = setup.depth_stride != 0u && clear_depth &&
|
|
constant_depth && setup.depth_pixels != nullptr;
|
|
const bool no_depth_effect = setup.depth_stride == 0u || fast_depth_clear ||
|
|
(!clear_depth && !setup.depth_test_enabled && !setup.depth_write_enabled);
|
|
const bool fast_clear = clear_mode && no_depth_effect &&
|
|
(no_color_clear || (full_color_clear && setup.color_pixels != nullptr));
|
|
if (fast_clear) {
|
|
const std::uint16_t depth_value = static_cast<std::uint16_t>(
|
|
std::clamp(static_cast<float>(a.z), 0.0f, 65535.0f));
|
|
const std::uint32_t packed32 = pack32(b.color);
|
|
const std::uint16_t packed16 = pack16(b.color, framebuffer_format);
|
|
const auto clear_rows = [&](std::int32_t row_first, std::int32_t row_last,
|
|
GeRenderStats &row_stats) {
|
|
for (std::int32_t y = row_first; y <= row_last; ++y) {
|
|
if (full_color_clear) {
|
|
std::uint8_t *row = setup.color_pixels +
|
|
(static_cast<std::size_t>(y) * framebuffer_stride +
|
|
static_cast<std::size_t>(x0)) * bpp;
|
|
if (framebuffer_format == 3u) {
|
|
std::fill_n(reinterpret_cast<std::uint32_t *>(row),
|
|
static_cast<std::size_t>(rectangle_width), packed32);
|
|
} else {
|
|
std::fill_n(reinterpret_cast<std::uint16_t *>(row),
|
|
static_cast<std::size_t>(rectangle_width), packed16);
|
|
}
|
|
}
|
|
if (fast_depth_clear) {
|
|
std::uint8_t *depth_row = setup.depth_pixels +
|
|
(static_cast<std::size_t>(y) * setup.depth_stride +
|
|
static_cast<std::size_t>(x0)) * 2u;
|
|
std::fill_n(reinterpret_cast<std::uint16_t *>(depth_row),
|
|
static_cast<std::size_t>(rectangle_width), depth_value);
|
|
}
|
|
row_stats.pixels_tested += static_cast<std::uint64_t>(rectangle_width);
|
|
row_stats.pixels_written += static_cast<std::uint64_t>(rectangle_width);
|
|
}
|
|
};
|
|
const auto invoke_clear = [&] {
|
|
if (covered < parallel_pixel_threshold() || pool.worker_count() <= 1u) {
|
|
clear_rows(y0, y1, stats);
|
|
return;
|
|
}
|
|
const unsigned slots = pool.worker_count();
|
|
std::array<GeRenderStats, RowWorkerPool::kMaxThreads> partial{};
|
|
pool.run(y0, y1, [&](unsigned index, std::int32_t first, std::int32_t last) {
|
|
clear_rows(first, last, partial[index]);
|
|
});
|
|
for (unsigned index = 0u; index < slots; ++index) {
|
|
stats.pixels_tested += partial[index].pixels_tested;
|
|
stats.pixels_written += partial[index].pixels_written;
|
|
}
|
|
};
|
|
if (phase_diag) {
|
|
PixelLoopTimer timer;
|
|
invoke_clear();
|
|
} else {
|
|
invoke_clear();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Rectangles carry the full-screen work in this game: a heavy frame spends
|
|
// about 14 ms here with no triangle drawn at all, because a 480x272 blit is
|
|
// 130000 fragments and this loop was the one raster path still running on a
|
|
// single thread. Rows are independent exactly as they are for triangles --
|
|
// every covered pixel is written once -- so splitting them is order-free.
|
|
const auto rasterize_rect_rows = [&](std::int32_t row_first, std::int32_t row_last,
|
|
GeRenderStats &row_stats) {
|
|
for (std::int32_t y = row_first; y <= row_last; ++y) {
|
|
const float ty = dy == 0.0f ? 0.0f : ((static_cast<float>(y) + 0.5f - a.y) / dy);
|
|
for (std::int32_t x = x0; x <= x1; ++x) {
|
|
++row_stats.pixels_tested;
|
|
const float tx = dx == 0.0f ? 0.0f : ((static_cast<float>(x) + 0.5f - a.x) / dx);
|
|
const float interpolation = std::clamp((tx + ty) * 0.5f, 0.0f, 1.0f);
|
|
Color source = b.color;
|
|
if (shaded_texture) {
|
|
const float q = a.q + (b.q - a.q) * interpolation;
|
|
if (!finite_float(q) || std::fabs(q) < 1.0e-20f) continue;
|
|
const float u = (a.u + (b.u - a.u) * tx) / q;
|
|
const float v = (a.v + (b.v - a.v) * ty) / q;
|
|
source = apply_texture_function(source, sample_texture(memory, setup.texture, u, v), setup);
|
|
}
|
|
if (!clear_mode && !alpha_test(source, setup)) continue;
|
|
const float zf = static_cast<float>(a.z) + (static_cast<float>(b.z) - a.z) * ((tx + ty) * 0.5f);
|
|
const std::uint16_t z = static_cast<std::uint16_t>(std::clamp(zf, 0.0f, 65535.0f));
|
|
if (!depth_test_and_write(memory, setup, x, y, z, clear_mode && clear_depth)) continue;
|
|
|
|
const std::size_t pixel_index =
|
|
static_cast<std::size_t>(y) * framebuffer_stride + static_cast<std::size_t>(x);
|
|
const std::uint32_t pixel_address = fb + static_cast<std::uint32_t>(pixel_index * bpp);
|
|
std::uint8_t *pixel = setup.color_pixels != nullptr
|
|
? setup.color_pixels + pixel_index * bpp : nullptr;
|
|
if (pixel == nullptr && !memory.contains(pixel_address, bpp)) continue;
|
|
Color destination = pixel != nullptr ? read_color_raw(pixel, framebuffer_format)
|
|
: read_color(memory, pixel_address, framebuffer_format);
|
|
if (clear_mode) {
|
|
if (!clear_color) { source.r = destination.r; source.g = destination.g; source.b = destination.b; }
|
|
if (!clear_alpha) source.a = destination.a;
|
|
} else {
|
|
source = blend_pixel(source, destination, setup);
|
|
}
|
|
if (pixel != nullptr) write_color_raw(pixel, framebuffer_format, source, clear_mode ? 0u : write_mask);
|
|
else write_color(memory, pixel_address, framebuffer_format, source, clear_mode ? 0u : write_mask);
|
|
++row_stats.pixels_written;
|
|
}
|
|
}
|
|
};
|
|
|
|
if (covered < parallel_pixel_threshold() || pool.worker_count() <= 1u) {
|
|
if (phase_diag) {
|
|
PixelLoopTimer timer;
|
|
rasterize_rect_rows(y0, y1, stats);
|
|
} else {
|
|
rasterize_rect_rows(y0, y1, stats);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const unsigned slots = pool.worker_count();
|
|
std::array<GeRenderStats, RowWorkerPool::kMaxThreads> partial{};
|
|
const auto run_parallel = [&] {
|
|
pool.run(y0, y1, [&](unsigned index, std::int32_t row_first, std::int32_t row_last) {
|
|
rasterize_rect_rows(row_first, row_last, partial[index]);
|
|
});
|
|
};
|
|
if (phase_diag) {
|
|
PixelLoopTimer timer;
|
|
run_parallel();
|
|
} else {
|
|
run_parallel();
|
|
}
|
|
for (unsigned index = 0u; index < slots; ++index) {
|
|
const GeRenderStats &item = partial[index];
|
|
stats.pixels_tested += item.pixels_tested;
|
|
stats.pixels_written += item.pixels_written;
|
|
}
|
|
}
|
|
|
|
|
|
float clip_distance(const Vertex &vertex, std::uint32_t plane) noexcept {
|
|
switch (plane) {
|
|
case 0u: return vertex.x + vertex.w;
|
|
case 1u: return vertex.w - vertex.x;
|
|
case 2u: return vertex.y + vertex.w;
|
|
case 3u: return vertex.w - vertex.y;
|
|
case 4u: return vertex.z + vertex.w;
|
|
default: return vertex.w - vertex.z;
|
|
}
|
|
}
|
|
|
|
Vertex interpolate_vertex(const Vertex &a, const Vertex &b, float t) noexcept {
|
|
Vertex out{};
|
|
auto lerp = [t](float x, float y) { return x + (y - x) * t; };
|
|
out.u = lerp(a.u, b.u);
|
|
out.v = lerp(a.v, b.v);
|
|
out.q = lerp(a.q, b.q);
|
|
out.x = lerp(a.x, b.x);
|
|
out.y = lerp(a.y, b.y);
|
|
out.z = lerp(a.z, b.z);
|
|
out.w = lerp(a.w, b.w);
|
|
out.fog_factor = lerp(a.fog_factor, b.fog_factor);
|
|
out.color = lerp_color(a.color, b.color, t);
|
|
return out;
|
|
}
|
|
|
|
// Sutherland-Hodgman against a plane grows a convex polygon by at most one
|
|
// vertex, so a triangle clipped against all six planes never exceeds nine.
|
|
// The polygon therefore fits in a fixed buffer. This used to allocate one
|
|
// std::vector per clip plane, and at tens of thousands of triangles per frame
|
|
// the allocator alone was a visible share of the frame.
|
|
struct ClipPolygon {
|
|
std::array<Vertex, 16> vertices;
|
|
std::size_t size{};
|
|
};
|
|
|
|
bool vertex_inside_all_planes(const Vertex &vertex, std::uint32_t plane_count) noexcept {
|
|
for (std::uint32_t plane = 0u; plane < plane_count; ++plane)
|
|
if (clip_distance(vertex, plane) < 0.0f) return false;
|
|
return true;
|
|
}
|
|
|
|
void clip_triangle(const Vertex &a, const Vertex &b, const Vertex &c,
|
|
bool depth_clip_enabled, ClipPolygon &result) {
|
|
const std::uint32_t plane_count = depth_clip_enabled ? 6u : 4u;
|
|
|
|
// Trivial accept. Nearly every triangle in a VCS frame lies completely
|
|
// inside the frustum, and for those the Sutherland-Hodgman loop below
|
|
// reproduces a, b, c in the original order: no edge crosses a plane, so no
|
|
// vertex is dropped and none is interpolated. Returning them directly is
|
|
// therefore bit-identical, and it skips default-initializing the two
|
|
// 16-vertex working polygons -- 1.4 KB of stores per triangle, paid roughly
|
|
// 25000 times per frame before any clipping work happened. A NaN
|
|
// coordinate fails the >= 0 test and falls through to the general path,
|
|
// exactly as it did before.
|
|
if (vertex_inside_all_planes(a, plane_count) &&
|
|
vertex_inside_all_planes(b, plane_count) &&
|
|
vertex_inside_all_planes(c, plane_count)) {
|
|
result.vertices[0] = a;
|
|
result.vertices[1] = b;
|
|
result.vertices[2] = c;
|
|
result.size = 3u;
|
|
return;
|
|
}
|
|
|
|
// Scratch for the clipped minority. The GE list runs on one thread; the
|
|
// raster worker pool never reaches this function, and thread_local keeps
|
|
// that true by construction.
|
|
static thread_local ClipPolygon buffers[2];
|
|
ClipPolygon *polygon = &buffers[0];
|
|
ClipPolygon *output = &buffers[1];
|
|
polygon->vertices[0] = a;
|
|
polygon->vertices[1] = b;
|
|
polygon->vertices[2] = c;
|
|
polygon->size = 3u;
|
|
|
|
for (std::uint32_t plane = 0u; plane < plane_count && polygon->size != 0u; ++plane) {
|
|
output->size = 0u;
|
|
Vertex previous = polygon->vertices[polygon->size - 1u];
|
|
float previous_distance = clip_distance(previous, plane);
|
|
bool previous_inside = previous_distance >= 0.0f;
|
|
for (std::size_t index = 0u; index < polygon->size; ++index) {
|
|
const Vertex ¤t = polygon->vertices[index];
|
|
const float current_distance = clip_distance(current, plane);
|
|
const bool current_inside = current_distance >= 0.0f;
|
|
if (current_inside != previous_inside) {
|
|
const float denominator = previous_distance - current_distance;
|
|
const float t = std::fabs(denominator) < 1.0e-20f ? 0.0f : previous_distance / denominator;
|
|
output->vertices[output->size++] =
|
|
interpolate_vertex(previous, current, std::clamp(t, 0.0f, 1.0f));
|
|
}
|
|
if (current_inside) output->vertices[output->size++] = current;
|
|
previous = current;
|
|
previous_distance = current_distance;
|
|
previous_inside = current_inside;
|
|
}
|
|
std::swap(polygon, output);
|
|
}
|
|
result.size = polygon->size;
|
|
for (std::size_t index = 0u; index < polygon->size; ++index)
|
|
result.vertices[index] = polygon->vertices[index];
|
|
}
|
|
|
|
bool viewport_transform(Vertex &vertex, const std::array<std::uint32_t, 256> &commands) noexcept {
|
|
if (!std::isfinite(vertex.w) || std::fabs(vertex.w) < 1.0e-12f) return false;
|
|
const float inv_w = 1.0f / vertex.w;
|
|
const float scale_x = decode_float24(data24(commands[0x42u]));
|
|
const float scale_y = decode_float24(data24(commands[0x43u]));
|
|
const float scale_z = decode_float24(data24(commands[0x44u]));
|
|
const float center_x = decode_float24(data24(commands[0x45u]));
|
|
const float center_y = decode_float24(data24(commands[0x46u]));
|
|
const float center_z = decode_float24(data24(commands[0x47u]));
|
|
// GE raster offsets are unsigned 12.4 fixed point. Interpreting bit 15
|
|
// as a sign bit moves legitimate PSP viewports by 4096 pixels.
|
|
const float offset_x = static_cast<float>(data24(commands[0x4Cu]) & 0xFFFFu) / 16.0f;
|
|
const float offset_y = static_cast<float>(data24(commands[0x4Du]) & 0xFFFFu) / 16.0f;
|
|
vertex.x = vertex.x * inv_w * scale_x + center_x - offset_x;
|
|
vertex.y = vertex.y * inv_w * scale_y + center_y - offset_y;
|
|
vertex.z = vertex.z * inv_w * scale_z + center_z;
|
|
vertex.inv_w = inv_w;
|
|
return std::isfinite(vertex.x) && std::isfinite(vertex.y) && std::isfinite(vertex.z);
|
|
}
|
|
|
|
bool point_inside_clip(const Vertex &vertex, bool depth_clip_enabled) noexcept {
|
|
if (!std::isfinite(vertex.w) || vertex.w <= 0.0f) return false;
|
|
const std::uint32_t plane_count = depth_clip_enabled ? 6u : 4u;
|
|
for (std::uint32_t plane = 0u; plane < plane_count; ++plane)
|
|
if (clip_distance(vertex, plane) < 0.0f) return false;
|
|
return true;
|
|
}
|
|
|
|
// Whether the depth test may run before the fragment is shaded.
|
|
//
|
|
// Only the alpha test sits between the two, and it can reject a fragment solely
|
|
// on the alpha the texture produced. When it is disabled -- or when clear mode
|
|
// bypasses it entirely -- nothing between them can discard the fragment, the
|
|
// depth test becomes the last thing that can, and running it first writes the
|
|
// depth buffer on precisely the same fragments, in the same order.
|
|
bool depth_precedes_shading(const FragmentSetup &setup) noexcept {
|
|
return setup.clear_mode || !setup.alpha_test_enabled;
|
|
}
|
|
|
|
// The bounds/scissor rejection and the depth test of write_fragment, split out
|
|
// so the pixel loop can discard an occluded fragment before paying for the
|
|
// perspective division, the interpolated colour and the texture fetch. This
|
|
// must stay byte-for-byte in step with the head of write_fragment below.
|
|
bool fragment_depth_prepass(psprecomp::GuestMemory &memory, const FragmentSetup &setup,
|
|
std::int32_t x, std::int32_t y, float zf, GeRenderStats &stats) {
|
|
if (!setup.valid || x < 0 || y < 0 ||
|
|
x >= static_cast<std::int32_t>(setup.framebuffer_stride)) {
|
|
return false;
|
|
}
|
|
if (x < setup.scissor_x0 || x > setup.scissor_x1 ||
|
|
y < setup.scissor_y0 || y > setup.scissor_y1) {
|
|
return false;
|
|
}
|
|
++stats.pixels_tested;
|
|
const std::uint16_t z = static_cast<std::uint16_t>(std::clamp(zf, 0.0f, 65535.0f));
|
|
return depth_test_and_write(memory, setup, x, y, z, setup.clear_mode && setup.clear_depth);
|
|
}
|
|
|
|
// `depth_resolved` says the caller already ran fragment_depth_prepass for this
|
|
// fragment: the bounds check, the tested counter and the depth test are then
|
|
// skipped here rather than repeated.
|
|
bool write_fragment(psprecomp::GuestMemory &memory,
|
|
const std::array<std::uint32_t, 256> &commands,
|
|
const FragmentSetup &setup,
|
|
std::int32_t x, std::int32_t y, float zf, float u, float v,
|
|
Color source, GeRenderStats &stats, bool depth_resolved = false) {
|
|
if (!depth_resolved) {
|
|
if (!setup.valid || x < 0 || y < 0 ||
|
|
x >= static_cast<std::int32_t>(setup.framebuffer_stride)) {
|
|
return false;
|
|
}
|
|
if (x < setup.scissor_x0 || x > setup.scissor_x1 ||
|
|
y < setup.scissor_y0 || y > setup.scissor_y1) {
|
|
return false;
|
|
}
|
|
++stats.pixels_tested;
|
|
}
|
|
const std::uint16_t z = static_cast<std::uint16_t>(std::clamp(zf, 0.0f, 65535.0f));
|
|
|
|
// Texture sampling is by far the most expensive step here -- with bilinear
|
|
// filtering it decodes four texels per fragment -- and in a city scene most
|
|
// fragments are occluded and end up discarded by the depth test anyway.
|
|
const bool depth_before_shading = depth_resolved || depth_precedes_shading(setup);
|
|
if (!depth_resolved && depth_before_shading &&
|
|
!depth_test_and_write(memory, setup, x, y, z, setup.clear_mode && setup.clear_depth))
|
|
return false;
|
|
|
|
if (setup.texture_enabled && !setup.clear_mode)
|
|
source = apply_texture_function(source, sample_texture(memory, setup.texture, u, v), setup);
|
|
if (!setup.clear_mode && !alpha_test(source, setup)) return false;
|
|
if (!depth_before_shading &&
|
|
!depth_test_and_write(memory, setup, x, y, z, setup.clear_mode && setup.clear_depth))
|
|
return false;
|
|
|
|
const std::size_t pixel_index =
|
|
static_cast<std::size_t>(y) * setup.framebuffer_stride + static_cast<std::size_t>(x);
|
|
std::uint8_t *const pixel = setup.color_pixels != nullptr
|
|
? setup.color_pixels + pixel_index * setup.framebuffer_bpp
|
|
: nullptr;
|
|
const std::uint32_t pixel_address =
|
|
setup.framebuffer_base + static_cast<std::uint32_t>(pixel_index) * setup.framebuffer_bpp;
|
|
if (pixel == nullptr && !memory.contains(pixel_address, setup.framebuffer_bpp)) return false;
|
|
|
|
Color destination = pixel != nullptr
|
|
? read_color_raw(pixel, setup.framebuffer_format)
|
|
: read_color(memory, pixel_address, setup.framebuffer_format);
|
|
if (setup.clear_mode) {
|
|
if (!setup.clear_color) { source.r = destination.r; source.g = destination.g; source.b = destination.b; }
|
|
if (!setup.clear_alpha) source.a = destination.a;
|
|
} else {
|
|
source = blend_pixel(source, destination, setup);
|
|
}
|
|
const std::uint32_t mask = setup.clear_mode ? 0u : setup.write_mask;
|
|
if (pixel != nullptr) write_color_raw(pixel, setup.framebuffer_format, source, mask);
|
|
else write_color(memory, pixel_address, setup.framebuffer_format, source, mask);
|
|
++stats.pixels_written;
|
|
return true;
|
|
}
|
|
|
|
float edge_function(const Vertex &a, const Vertex &b, float x, float y) noexcept {
|
|
return (x - a.x) * (b.y - a.y) - (y - a.y) * (b.x - a.x);
|
|
}
|
|
|
|
Color perspective_color(const Vertex &a, const Vertex &b, const Vertex &c,
|
|
float l0, float l1, float l2, float denominator) noexcept {
|
|
auto numerator = [&](std::uint8_t ca, std::uint8_t cb, std::uint8_t cc) {
|
|
return l0 * static_cast<float>(ca) * a.inv_w +
|
|
l1 * static_cast<float>(cb) * b.inv_w +
|
|
l2 * static_cast<float>(cc) * c.inv_w;
|
|
};
|
|
float values[4]{};
|
|
divide4_same_denominator(numerator(a.color.r, b.color.r, c.color.r),
|
|
numerator(a.color.g, b.color.g, c.color.g),
|
|
numerator(a.color.b, b.color.b, c.color.b),
|
|
numerator(a.color.a, b.color.a, c.color.a),
|
|
denominator, values);
|
|
return {round_clamp_to_byte(values[0]), round_clamp_to_byte(values[1]),
|
|
round_clamp_to_byte(values[2]), round_clamp_to_byte(values[3])};
|
|
}
|
|
|
|
struct PreparedScreenTriangle {
|
|
Vertex a{};
|
|
Vertex b{};
|
|
Vertex c{};
|
|
Color provoking_color{};
|
|
float area{};
|
|
std::int32_t min_x{};
|
|
std::int32_t max_x{};
|
|
std::int32_t min_y{};
|
|
std::int32_t max_y{};
|
|
bool texture_enabled{};
|
|
bool early_depth{};
|
|
bool flat_shading{};
|
|
#if PSPRECOMP_GE_X86_SIMD
|
|
__m128 edge_ax{};
|
|
__m128 edge_ay{};
|
|
__m128 edge_dx{};
|
|
__m128 edge_dy{};
|
|
#endif
|
|
};
|
|
|
|
std::uint32_t pack_gpu_color(Color color) noexcept {
|
|
return static_cast<std::uint32_t>(color.r) |
|
|
(static_cast<std::uint32_t>(color.g) << 8u) |
|
|
(static_cast<std::uint32_t>(color.b) << 16u) |
|
|
(static_cast<std::uint32_t>(color.a) << 24u);
|
|
}
|
|
|
|
std::uint32_t pack_gpu_alpha_control(const GeGpuDrawDescriptor &draw) noexcept {
|
|
return static_cast<std::uint32_t>(draw.alpha_test_enabled ? 1u : 0u) |
|
|
((draw.alpha_function & 7u) << 8u) |
|
|
((draw.alpha_reference & 0xFFu) << 16u) |
|
|
((draw.alpha_mask & 0xFFu) << 24u);
|
|
}
|
|
|
|
std::uint32_t pack_gpu_fog_control(const GeGpuDrawDescriptor &draw) noexcept {
|
|
return (draw.fog_color & 0x00FFFFFFu) |
|
|
(static_cast<std::uint32_t>(draw.fog_enabled ? 0xFFu : 0u) << 24u);
|
|
}
|
|
|
|
Color gpu_draw_debug_color(const GeGpuDrawDescriptor &draw) noexcept {
|
|
std::uint32_t hash = draw.texture_address ^ (draw.texture_address >> 11u) ^
|
|
(draw.texture_buffer_width * 0x9E3779B9u) ^
|
|
(draw.texture_format * 0x85EBCA6Bu) ^
|
|
(draw.primitive * 0xC2B2AE35u) ^ draw.vertex_count;
|
|
hash ^= hash >> 16u;
|
|
hash *= 0x7FEB352Du;
|
|
hash ^= hash >> 15u;
|
|
return {
|
|
static_cast<std::uint8_t>(48u + (hash & 0xCFu)),
|
|
static_cast<std::uint8_t>(48u + ((hash >> 8u) & 0xCFu)),
|
|
static_cast<std::uint8_t>(48u + ((hash >> 16u) & 0xCFu)),
|
|
255u,
|
|
};
|
|
}
|
|
|
|
bool gpu_geometry_debug_colors_enabled() noexcept {
|
|
static const bool enabled = [] {
|
|
const char *value = std::getenv("PSPRECOMP_GE_GPU_GEOMETRY_DEBUG_COLORS");
|
|
return value != nullptr && *value != '\0' && std::strcmp(value, "0") != 0;
|
|
}();
|
|
return enabled;
|
|
}
|
|
|
|
|
|
bool gpu_force_white_vertex_colors_enabled() noexcept {
|
|
static const bool enabled = [] {
|
|
const char *value = std::getenv("PSPRECOMP_GE_GPU_FORCE_WHITE_VERTEX");
|
|
return value != nullptr && *value != '\0' && std::strcmp(value, "0") != 0;
|
|
}();
|
|
return enabled;
|
|
}
|
|
|
|
Color gpu_texture_debug_color(const GeGpuDrawDescriptor &draw, Color lighting) noexcept {
|
|
// Stage 24 has not uploaded PSP textures yet. Rendering their vertices as
|
|
// black would hide the fact that the real game geometry is being rasterized
|
|
// at the configured internal resolution, because many REPLACE-mode draws
|
|
// deliberately carry black/unused vertex colors. Until descriptor-backed
|
|
// texture sampling lands, use a stable color derived from the actual guest
|
|
// texture address. MODULATE-like paths retain the vertex lighting.
|
|
std::uint32_t hash = draw.texture_address ^ (draw.texture_address >> 11u) ^
|
|
(draw.texture_buffer_width * 0x9E3779B9u) ^
|
|
(draw.texture_format * 0x85EBCA6Bu);
|
|
hash ^= hash >> 16u;
|
|
hash *= 0x7FEB352Du;
|
|
hash ^= hash >> 15u;
|
|
const Color texture_color{
|
|
static_cast<std::uint8_t>(72u + (hash & 0xB7u)),
|
|
static_cast<std::uint8_t>(72u + ((hash >> 8u) & 0xB7u)),
|
|
static_cast<std::uint8_t>(72u + ((hash >> 16u) & 0xB7u)),
|
|
255u,
|
|
};
|
|
// PSP GU_TFX_REPLACE is 3. In that mode the vertex RGB is intentionally
|
|
// irrelevant, so return the diagnostic texture color directly.
|
|
if (draw.texture_function == 3u ||
|
|
(lighting.r <= 4u && lighting.g <= 4u && lighting.b <= 4u))
|
|
return texture_color;
|
|
auto modulate = [](std::uint8_t a, std::uint8_t b) noexcept {
|
|
return static_cast<std::uint8_t>((static_cast<unsigned>(a) * b + 127u) / 255u);
|
|
};
|
|
return {modulate(texture_color.r, lighting.r),
|
|
modulate(texture_color.g, lighting.g),
|
|
modulate(texture_color.b, lighting.b), 255u};
|
|
}
|
|
|
|
GeGpuDrawDescriptor gpu_effective_draw_descriptor(GeGpuDrawDescriptor draw) noexcept {
|
|
if (!draw.clear_mode) return draw;
|
|
draw.texture_enabled = false;
|
|
draw.blend_enabled = false;
|
|
draw.alpha_test_enabled = false;
|
|
draw.fog_enabled = false;
|
|
draw.depth_test_enabled = false;
|
|
draw.depth_write_enabled = draw.clear_depth;
|
|
// PSP write-mask bytes use 0 for writable and 0xFF for fully masked.
|
|
draw.color_write_mask =
|
|
(draw.clear_color ? 0x00000000u : 0x00FFFFFFu) |
|
|
(draw.clear_alpha ? 0x00000000u : 0xFF000000u);
|
|
return draw;
|
|
}
|
|
|
|
void accumulate_gpu_prepared_triangles(
|
|
const GeGpuDrawDescriptor &draw,
|
|
const std::vector<PreparedScreenTriangle> &triangles) {
|
|
if (!ge_gpu_backend_graphics_ready() || triangles.empty()) return;
|
|
const GeGpuDrawDescriptor effective_draw = gpu_effective_draw_descriptor(draw);
|
|
const bool sampled_texture_ready =
|
|
effective_draw.texture_enabled && ge_gpu_backend_texture_available(effective_draw);
|
|
// Reused: one allocation per draw call, about 1200 per heavy vblank, for a
|
|
// buffer that is handed to the backend and immediately discarded.
|
|
static thread_local std::vector<GeGpuVertex> vertices;
|
|
try {
|
|
vertices.clear();
|
|
vertices.reserve(triangles.size() * 3u);
|
|
for (const PreparedScreenTriangle &triangle : triangles) {
|
|
Color ca = triangle.flat_shading ? triangle.provoking_color : triangle.a.color;
|
|
Color cb = triangle.flat_shading ? triangle.provoking_color : triangle.b.color;
|
|
Color cc = triangle.flat_shading ? triangle.provoking_color : triangle.c.color;
|
|
if (gpu_force_white_vertex_colors_enabled()) {
|
|
ca = cb = cc = Color{255u, 255u, 255u, 255u};
|
|
} else if (gpu_geometry_debug_colors_enabled()) {
|
|
ca = cb = cc = gpu_draw_debug_color(effective_draw);
|
|
} else if (effective_draw.texture_enabled) {
|
|
if (sampled_texture_ready) {
|
|
// Stage 31 implements the complete texture-function switch
|
|
// in the fragment shader; preserve the real interpolated
|
|
// vertex color for every function, including REPLACE.
|
|
} else {
|
|
ca = gpu_texture_debug_color(effective_draw, ca);
|
|
cb = gpu_texture_debug_color(effective_draw, cb);
|
|
cc = gpu_texture_debug_color(effective_draw, cc);
|
|
}
|
|
}
|
|
vertices.push_back({triangle.a.x, triangle.a.y, triangle.a.z, triangle.a.w,
|
|
pack_gpu_color(ca), triangle.a.u, triangle.a.v,
|
|
pack_gpu_alpha_control(effective_draw), 0u, effective_draw.texture_env,
|
|
triangle.a.fog_factor, pack_gpu_fog_control(effective_draw), triangle.a.q});
|
|
vertices.push_back({triangle.b.x, triangle.b.y, triangle.b.z, triangle.b.w,
|
|
pack_gpu_color(cb), triangle.b.u, triangle.b.v,
|
|
pack_gpu_alpha_control(effective_draw), 0u, effective_draw.texture_env,
|
|
triangle.b.fog_factor, pack_gpu_fog_control(effective_draw), triangle.b.q});
|
|
vertices.push_back({triangle.c.x, triangle.c.y, triangle.c.z, triangle.c.w,
|
|
pack_gpu_color(cc), triangle.c.u, triangle.c.v,
|
|
pack_gpu_alpha_control(effective_draw), 0u, effective_draw.texture_env,
|
|
triangle.c.fog_factor, pack_gpu_fog_control(effective_draw), triangle.c.q});
|
|
}
|
|
ge_gpu_backend_accumulate_color_triangles(effective_draw, vertices);
|
|
} catch (...) {
|
|
// The reference software path must remain authoritative even if the
|
|
// experimental high-resolution preview runs out of host memory.
|
|
}
|
|
}
|
|
|
|
|
|
void accumulate_gpu_rectangle(const GeGpuDrawDescriptor &draw,
|
|
const Vertex &a, const Vertex &b) {
|
|
if (!ge_gpu_backend_graphics_ready()) return;
|
|
const GeGpuDrawDescriptor effective_draw = gpu_effective_draw_descriptor(draw);
|
|
const bool sampled_texture_ready = effective_draw.texture_enabled &&
|
|
ge_gpu_backend_texture_available(effective_draw);
|
|
|
|
Color color = b.color;
|
|
if (!effective_draw.clear_mode && gpu_force_white_vertex_colors_enabled()) {
|
|
color = Color{255u, 255u, 255u, 255u};
|
|
} else if (!effective_draw.clear_mode && gpu_geometry_debug_colors_enabled()) {
|
|
color = gpu_draw_debug_color(effective_draw);
|
|
} else if (effective_draw.texture_enabled && !sampled_texture_ready) {
|
|
color = gpu_texture_debug_color(effective_draw, color);
|
|
}
|
|
|
|
// PSP rectangle primitives use the second endpoint color for every fragment.
|
|
// U varies only along X, V only along Y, while depth/Q lie on the diagonal
|
|
// plane between the two endpoints. Two triangles with midpoint diagonal
|
|
// values preserve that plane and keep draw order with the 3D batches.
|
|
const float mid_z = (a.z + b.z) * 0.5f;
|
|
const float mid_q = (a.q + b.q) * 0.5f;
|
|
const std::uint32_t packed = pack_gpu_color(color);
|
|
const std::uint32_t alpha = pack_gpu_alpha_control(effective_draw);
|
|
const std::uint32_t fog = pack_gpu_fog_control(effective_draw);
|
|
const auto make = [&](float x, float y, float z, float u, float v,
|
|
float q, float fog_factor) {
|
|
if (!std::isfinite(q) || std::fabs(q) < 1.0e-20f) q = 1.0f;
|
|
return GeGpuVertex{x, y, z, 1.0f, packed, u, v,
|
|
alpha, 0u, effective_draw.texture_env, fog_factor, fog, q};
|
|
};
|
|
|
|
const GeGpuVertex p00 = make(a.x, a.y, a.z, a.u, a.v, a.q, a.fog_factor);
|
|
const GeGpuVertex p10 = make(b.x, a.y, mid_z, b.u, a.v, mid_q,
|
|
(a.fog_factor + b.fog_factor) * 0.5f);
|
|
const GeGpuVertex p11 = make(b.x, b.y, b.z, b.u, b.v, b.q, b.fog_factor);
|
|
const GeGpuVertex p01 = make(a.x, b.y, mid_z, a.u, b.v, mid_q,
|
|
(a.fog_factor + b.fog_factor) * 0.5f);
|
|
const std::array<GeGpuVertex, 6> triangles{{p00, p10, p11, p00, p11, p01}};
|
|
ge_gpu_backend_accumulate_color_triangles(effective_draw, triangles);
|
|
}
|
|
|
|
bool prepare_screen_triangle(const std::array<std::uint32_t, 256> &commands,
|
|
const FragmentSetup &setup,
|
|
const Vertex &a, const Vertex &b, const Vertex &c,
|
|
Color provoking_color, GeRenderStats &stats,
|
|
PreparedScreenTriangle &prepared) {
|
|
record_screen_vertex(stats, a);
|
|
record_screen_vertex(stats, b);
|
|
record_screen_vertex(stats, c);
|
|
const float area = edge_function(a, b, c.x, c.y);
|
|
if (!std::isfinite(area) || std::fabs(area) < 1.0e-8f) return false;
|
|
|
|
const bool clear_mode = (data24(commands[0xD3u]) & 1u) != 0u;
|
|
if (!clear_mode && (data24(commands[0x1Du]) & 1u) != 0u) {
|
|
const bool counter_clockwise = area < 0.0f;
|
|
const bool accept_counter_clockwise = (data24(commands[0x9Bu]) & 1u) != 0u;
|
|
if (counter_clockwise != accept_counter_clockwise) {
|
|
if (g_collect_ge_render_stats) ++stats.culled_triangles;
|
|
return false;
|
|
}
|
|
}
|
|
const bool flat_shading = !clear_mode && (data24(commands[0x50u]) & 1u) == 0u;
|
|
if (flat_shading && g_collect_ge_render_stats) ++stats.flat_shaded_primitives;
|
|
|
|
if (!setup.valid) return false;
|
|
const std::int32_t min_x = std::max(setup.scissor_x0,
|
|
static_cast<std::int32_t>(std::floor(std::min(a.x, std::min(b.x, c.x)))));
|
|
const std::int32_t max_x = std::min(setup.scissor_x1,
|
|
static_cast<std::int32_t>(std::ceil(std::max(a.x, std::max(b.x, c.x)))));
|
|
const std::int32_t min_y = std::max(setup.scissor_y0,
|
|
static_cast<std::int32_t>(std::floor(std::min(a.y, std::min(b.y, c.y)))));
|
|
const std::int32_t max_y = std::min(setup.scissor_y1,
|
|
static_cast<std::int32_t>(std::ceil(std::max(a.y, std::max(b.y, c.y)))));
|
|
if (min_x > max_x || min_y > max_y) return false;
|
|
|
|
prepared.a = a;
|
|
prepared.b = b;
|
|
prepared.c = c;
|
|
prepared.provoking_color = provoking_color;
|
|
prepared.area = area;
|
|
prepared.min_x = min_x;
|
|
prepared.max_x = max_x;
|
|
prepared.min_y = min_y;
|
|
prepared.max_y = max_y;
|
|
prepared.texture_enabled = setup.texture_enabled && !clear_mode;
|
|
prepared.early_depth = depth_precedes_shading(setup);
|
|
prepared.flat_shading = flat_shading;
|
|
#if PSPRECOMP_GE_X86_SIMD
|
|
prepared.edge_ax = _mm_set_ps(0.0f, a.x, c.x, b.x);
|
|
prepared.edge_ay = _mm_set_ps(0.0f, a.y, c.y, b.y);
|
|
prepared.edge_dx = _mm_set_ps(0.0f, b.x - a.x, a.x - c.x, c.x - b.x);
|
|
prepared.edge_dy = _mm_set_ps(0.0f, b.y - a.y, a.y - c.y, c.y - b.y);
|
|
#endif
|
|
return true;
|
|
}
|
|
|
|
bool prepare_gpu_only_screen_triangle(const std::array<std::uint32_t, 256> &commands,
|
|
const FragmentSetup &setup,
|
|
const Vertex &a, const Vertex &b, const Vertex &c,
|
|
Color provoking_color, GeRenderStats &stats,
|
|
PreparedScreenTriangle &prepared) {
|
|
// Vulkan still needs viewport transform and PSP culling, but when the CPU
|
|
// raster is skipped it does not need pixel bounds, edge SIMD coefficients,
|
|
// early-depth metadata or any other software-fragment setup.
|
|
record_screen_vertex(stats, a);
|
|
record_screen_vertex(stats, b);
|
|
record_screen_vertex(stats, c);
|
|
const float area = edge_function(a, b, c.x, c.y);
|
|
if (!std::isfinite(area) || std::fabs(area) < 1.0e-8f || !setup.valid) return false;
|
|
const bool clear_mode = (data24(commands[0xD3u]) & 1u) != 0u;
|
|
if (!clear_mode && (data24(commands[0x1Du]) & 1u) != 0u) {
|
|
const bool counter_clockwise = area < 0.0f;
|
|
const bool accept_counter_clockwise = (data24(commands[0x9Bu]) & 1u) != 0u;
|
|
if (counter_clockwise != accept_counter_clockwise) {
|
|
if (g_collect_ge_render_stats) ++stats.culled_triangles;
|
|
return false;
|
|
}
|
|
}
|
|
const bool flat_shading = !clear_mode && (data24(commands[0x50u]) & 1u) == 0u;
|
|
if (flat_shading && g_collect_ge_render_stats) ++stats.flat_shaded_primitives;
|
|
prepared.a = a;
|
|
prepared.b = b;
|
|
prepared.c = c;
|
|
prepared.provoking_color = provoking_color;
|
|
prepared.area = area;
|
|
prepared.flat_shading = flat_shading;
|
|
return true;
|
|
}
|
|
|
|
void append_prepared_triangles(const std::array<std::uint32_t, 256> &commands,
|
|
const FragmentSetup &setup,
|
|
const Vertex &a, const Vertex &b, const Vertex &c,
|
|
const Vertex &provoking, bool through,
|
|
GeRenderStats &stats,
|
|
std::vector<PreparedScreenTriangle> &prepared,
|
|
bool gpu_only = false) {
|
|
// Built in place: a PreparedScreenTriangle is about 260 bytes, so the old
|
|
// "value-initialize a local, then copy it into the vector" cost three full
|
|
// passes over it per triangle kept.
|
|
const auto emplace_prepared = [&](const Vertex &va, const Vertex &vb, const Vertex &vc) {
|
|
prepared.emplace_back();
|
|
const bool kept = gpu_only
|
|
? prepare_gpu_only_screen_triangle(commands, setup, va, vb, vc,
|
|
provoking.color, stats, prepared.back())
|
|
: prepare_screen_triangle(commands, setup, va, vb, vc, provoking.color,
|
|
stats, prepared.back());
|
|
if (!kept) prepared.pop_back();
|
|
};
|
|
|
|
if (through) {
|
|
emplace_prepared(a, b, c);
|
|
return;
|
|
}
|
|
|
|
const bool depth_clip_enabled = (data24(commands[0x1Cu]) & 1u) != 0u;
|
|
// Same reason as the scratch inside clip_triangle: a plain local would
|
|
// default-initialize 16 vertices on every triangle just to have the first
|
|
// three overwritten.
|
|
static thread_local ClipPolygon polygon;
|
|
clip_triangle(a, b, c, depth_clip_enabled, polygon);
|
|
if (polygon.size < 3u) return;
|
|
for (std::size_t index = 0u; index < polygon.size; ++index)
|
|
if (!viewport_transform(polygon.vertices[index], commands)) return;
|
|
for (std::size_t index = 1u; index + 1u < polygon.size; ++index)
|
|
emplace_prepared(polygon.vertices[0], polygon.vertices[index],
|
|
polygon.vertices[index + 1u]);
|
|
}
|
|
|
|
void rasterize_prepared_triangle_rows(psprecomp::GuestMemory &memory,
|
|
const std::array<std::uint32_t, 256> &commands,
|
|
const FragmentSetup &setup,
|
|
const PreparedScreenTriangle &triangle,
|
|
std::int32_t row_first, std::int32_t row_last,
|
|
GeRenderStats &row_stats) {
|
|
row_first = std::max(row_first, triangle.min_y);
|
|
row_last = std::min(row_last, triangle.max_y);
|
|
if (row_first > row_last) return;
|
|
|
|
const Vertex &a = triangle.a;
|
|
const Vertex &b = triangle.b;
|
|
const Vertex &c = triangle.c;
|
|
#if PSPRECOMP_GE_X86_SIMD
|
|
const __m128 area_vector = _mm_set1_ps(triangle.area);
|
|
const __m128 zero_vector = _mm_setzero_ps();
|
|
const bool positive_area = triangle.area > 0.0f;
|
|
#endif
|
|
for (std::int32_t y = row_first; y <= row_last; ++y) {
|
|
const float py = static_cast<float>(y) + 0.5f;
|
|
#if PSPRECOMP_GE_X86_SIMD
|
|
const __m128 edge_y_term = _mm_mul_ps(
|
|
_mm_sub_ps(_mm_set1_ps(py), triangle.edge_ay), triangle.edge_dx);
|
|
#endif
|
|
for (std::int32_t x = triangle.min_x; x <= triangle.max_x; ++x) {
|
|
const float px = static_cast<float>(x) + 0.5f;
|
|
float l0{}, l1{}, l2{};
|
|
#if PSPRECOMP_GE_X86_SIMD
|
|
const __m128 edges = _mm_sub_ps(
|
|
_mm_mul_ps(_mm_sub_ps(_mm_set1_ps(px), triangle.edge_ax), triangle.edge_dy),
|
|
edge_y_term);
|
|
const __m128 outside = positive_area
|
|
? _mm_cmplt_ps(edges, zero_vector)
|
|
: _mm_cmpgt_ps(edges, zero_vector);
|
|
if ((_mm_movemask_ps(outside) & 0x7) != 0) continue;
|
|
alignas(16) float barycentric[4];
|
|
_mm_store_ps(barycentric, _mm_div_ps(edges, area_vector));
|
|
l0 = barycentric[0];
|
|
l1 = barycentric[1];
|
|
l2 = barycentric[2];
|
|
#else
|
|
const float e0 = edge_function(b, c, px, py);
|
|
const float e1 = edge_function(c, a, px, py);
|
|
const float e2 = edge_function(a, b, px, py);
|
|
if (triangle.area > 0.0f) {
|
|
if (e0 < 0.0f || e1 < 0.0f || e2 < 0.0f) continue;
|
|
} else if (e0 > 0.0f || e1 > 0.0f || e2 > 0.0f) {
|
|
continue;
|
|
}
|
|
divide3_same_denominator(e0, e1, e2, triangle.area, l0, l1, l2);
|
|
#endif
|
|
const float denominator = l0 * a.inv_w + l1 * b.inv_w + l2 * c.inv_w;
|
|
if (!finite_float(denominator) || std::fabs(denominator) < 1.0e-20f) continue;
|
|
float texture_denominator = 1.0f;
|
|
if (triangle.texture_enabled) {
|
|
texture_denominator = l0 * a.q * a.inv_w + l1 * b.q * b.inv_w + l2 * c.q * c.inv_w;
|
|
if (!finite_float(texture_denominator) || std::fabs(texture_denominator) < 1.0e-20f) continue;
|
|
}
|
|
const float z = l0 * a.z + l1 * b.z + l2 * c.z;
|
|
if (triangle.early_depth &&
|
|
!fragment_depth_prepass(memory, setup, x, y, z, row_stats)) continue;
|
|
float u = 0.0f;
|
|
float v = 0.0f;
|
|
if (triangle.texture_enabled) {
|
|
const float u_numerator = l0 * a.u * a.inv_w + l1 * b.u * b.inv_w + l2 * c.u * c.inv_w;
|
|
const float v_numerator = l0 * a.v * a.inv_w + l1 * b.v * b.inv_w + l2 * c.v * c.inv_w;
|
|
divide2_same_denominator(u_numerator, v_numerator, texture_denominator, u, v);
|
|
}
|
|
const Color color = triangle.flat_shading
|
|
? triangle.provoking_color
|
|
: perspective_color(a, b, c, l0, l1, l2, denominator);
|
|
write_fragment(memory, commands, setup, x, y, z, u, v, color,
|
|
row_stats, triangle.early_depth);
|
|
}
|
|
}
|
|
}
|
|
|
|
void rasterize_prepared_triangles(psprecomp::GuestMemory &memory,
|
|
const std::array<std::uint32_t, 256> &commands,
|
|
const FragmentSetup &setup,
|
|
const std::vector<PreparedScreenTriangle> &triangles,
|
|
GeRenderStats &stats) {
|
|
if (triangles.empty()) return;
|
|
if (software_raster_skipped(commands)) return;
|
|
|
|
std::int32_t min_y = triangles.front().min_y;
|
|
std::int32_t max_y = triangles.front().max_y;
|
|
std::int64_t covered = 0;
|
|
for (const PreparedScreenTriangle &triangle : triangles) {
|
|
min_y = std::min(min_y, triangle.min_y);
|
|
max_y = std::max(max_y, triangle.max_y);
|
|
covered += static_cast<std::int64_t>(triangle.max_x - triangle.min_x + 1) *
|
|
static_cast<std::int64_t>(triangle.max_y - triangle.min_y + 1);
|
|
}
|
|
|
|
const bool phase_diag = ge_phase_diag_enabled();
|
|
if (phase_diag) g_ge_triangle_count += triangles.size();
|
|
|
|
const auto rasterize_serial = [&](GeRenderStats &target) {
|
|
for (const PreparedScreenTriangle &triangle : triangles)
|
|
rasterize_prepared_triangle_rows(memory, commands, setup, triangle,
|
|
triangle.min_y, triangle.max_y, target);
|
|
};
|
|
|
|
RowWorkerPool &pool = RowWorkerPool::instance();
|
|
if (covered < parallel_pixel_threshold() || pool.worker_count() <= 1u ||
|
|
min_y >= max_y) {
|
|
if (phase_diag) {
|
|
PixelLoopTimer timer;
|
|
rasterize_serial(stats);
|
|
} else {
|
|
rasterize_serial(stats);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const unsigned slots = pool.worker_count();
|
|
std::array<GeRenderStats, RowWorkerPool::kMaxThreads> partial{};
|
|
const auto run_parallel = [&] {
|
|
pool.run(min_y, max_y, [&](unsigned index,
|
|
std::int32_t row_first, std::int32_t row_last) {
|
|
// Every participant owns distinct rows. Walking the complete draw
|
|
// call in original triangle order therefore preserves blending and
|
|
// depth order for every pixel while replacing thousands of
|
|
// per-triangle joins with one draw-call join.
|
|
for (const PreparedScreenTriangle &triangle : triangles) {
|
|
if (row_last < triangle.min_y || row_first > triangle.max_y) continue;
|
|
rasterize_prepared_triangle_rows(memory, commands, setup, triangle,
|
|
row_first, row_last, partial[index]);
|
|
}
|
|
});
|
|
};
|
|
if (phase_diag) {
|
|
PixelLoopTimer timer;
|
|
run_parallel();
|
|
} else {
|
|
run_parallel();
|
|
}
|
|
for (unsigned index = 0u; index < slots; ++index) {
|
|
stats.pixels_tested += partial[index].pixels_tested;
|
|
stats.pixels_written += partial[index].pixels_written;
|
|
}
|
|
}
|
|
|
|
void rasterize_triangle(psprecomp::GuestMemory &memory,
|
|
const std::array<std::uint32_t, 256> &commands,
|
|
const FragmentSetup &setup,
|
|
const Vertex &a, const Vertex &b, const Vertex &c,
|
|
const Vertex &provoking, bool through, GeRenderStats &stats) {
|
|
std::vector<PreparedScreenTriangle> triangles;
|
|
triangles.reserve(4u);
|
|
append_prepared_triangles(commands, setup, a, b, c, provoking, through, stats, triangles);
|
|
rasterize_prepared_triangles(memory, commands, setup, triangles, stats);
|
|
}
|
|
|
|
void rasterize_point(psprecomp::GuestMemory &memory,
|
|
const std::array<std::uint32_t, 256> &commands,
|
|
const FragmentSetup &setup,
|
|
Vertex vertex, bool through, GeRenderStats &stats) {
|
|
if (!through) {
|
|
const bool depth_clip_enabled = (data24(commands[0x1Cu]) & 1u) != 0u;
|
|
if (!point_inside_clip(vertex, depth_clip_enabled) || !viewport_transform(vertex, commands)) return;
|
|
}
|
|
record_screen_vertex(stats, vertex);
|
|
const bool texture_enabled = (data24(commands[0x1Eu]) & 1u) != 0u && (data24(commands[0xD3u]) & 1u) == 0u;
|
|
if (texture_enabled && (!std::isfinite(vertex.q) || std::fabs(vertex.q) < 1.0e-20f)) return;
|
|
const float q = texture_enabled ? vertex.q : 1.0f;
|
|
write_fragment(memory, commands, setup, static_cast<std::int32_t>(std::floor(vertex.x)),
|
|
static_cast<std::int32_t>(std::floor(vertex.y)), vertex.z,
|
|
vertex.u / q, vertex.v / q, vertex.color, stats);
|
|
}
|
|
|
|
void rasterize_line(psprecomp::GuestMemory &memory,
|
|
const std::array<std::uint32_t, 256> &commands,
|
|
const FragmentSetup &setup,
|
|
Vertex a, Vertex b, bool through, GeRenderStats &stats) {
|
|
if (!through) {
|
|
// A conservative first line path: fully clipped endpoints are rejected. Triangle
|
|
// clipping is complete; homogeneous line clipping can be added independently.
|
|
const bool depth_clip_enabled = (data24(commands[0x1Cu]) & 1u) != 0u;
|
|
if (!point_inside_clip(a, depth_clip_enabled) || !point_inside_clip(b, depth_clip_enabled) ||
|
|
!viewport_transform(a, commands) || !viewport_transform(b, commands)) return;
|
|
}
|
|
record_screen_vertex(stats, a);
|
|
record_screen_vertex(stats, b);
|
|
const float dx = b.x - a.x;
|
|
const float dy = b.y - a.y;
|
|
const std::int32_t steps = static_cast<std::int32_t>(std::ceil(std::max(std::fabs(dx), std::fabs(dy))));
|
|
if (steps <= 0) {
|
|
rasterize_point(memory, commands, setup, a, true, stats);
|
|
return;
|
|
}
|
|
if ((data24(commands[0xD3u]) & 1u) == 0u && (data24(commands[0x50u]) & 1u) == 0u)
|
|
++stats.flat_shaded_primitives;
|
|
const FragmentSetup &line_setup = setup;
|
|
const bool line_texture_enabled =
|
|
line_setup.texture_enabled && (data24(commands[0xD3u]) & 1u) == 0u;
|
|
const bool line_flat_shading =
|
|
(data24(commands[0xD3u]) & 1u) == 0u && (data24(commands[0x50u]) & 1u) == 0u;
|
|
for (std::int32_t i = 0; i <= steps; ++i) {
|
|
const float t = static_cast<float>(i) / static_cast<float>(steps);
|
|
const float one_minus_t = 1.0f - t;
|
|
const float denominator = one_minus_t * a.inv_w + t * b.inv_w;
|
|
if (std::fabs(denominator) < 1.0e-20f) continue;
|
|
const float texture_denominator = line_texture_enabled
|
|
? one_minus_t * a.q * a.inv_w + t * b.q * b.inv_w
|
|
: denominator;
|
|
if (!std::isfinite(texture_denominator) || std::fabs(texture_denominator) < 1.0e-20f) continue;
|
|
const float u = (one_minus_t * a.u * a.inv_w + t * b.u * b.inv_w) / texture_denominator;
|
|
const float v = (one_minus_t * a.v * a.inv_w + t * b.v * b.inv_w) / texture_denominator;
|
|
const Color color = line_flat_shading ? b.color : lerp_color(a.color, b.color, t);
|
|
write_fragment(memory, commands, line_setup,
|
|
static_cast<std::int32_t>(std::floor(a.x + dx * t)),
|
|
static_cast<std::int32_t>(std::floor(a.y + dy * t)),
|
|
a.z + (b.z - a.z) * t, u, v, color, stats);
|
|
}
|
|
}
|
|
|
|
std::uint32_t read_index(const psprecomp::GuestMemory &memory, std::uint32_t base,
|
|
std::uint32_t index_type, std::uint32_t element) {
|
|
switch (index_type) {
|
|
case 1u: return memory.aot_load8(base + element);
|
|
case 2u: return memory.aot_load16(base + element * 2u);
|
|
case 3u: return memory.aot_load32(base + element * 4u);
|
|
default: return element;
|
|
}
|
|
}
|
|
|
|
// Stage 42: GE index streams are contiguous by definition. Resolve guest RAM
|
|
// once per draw instead of canonicalizing and bounds-checking every 8/16/32-bit
|
|
// index. Manual little-endian assembly is alignment-safe and matches PSP RAM.
|
|
struct IndexStreamReader {
|
|
const psprecomp::GuestMemory *memory{};
|
|
const std::uint8_t *raw{};
|
|
std::uint32_t base{};
|
|
std::uint32_t type{};
|
|
|
|
[[nodiscard]] std::uint32_t operator()(std::uint32_t element) const noexcept {
|
|
if (type == 0u) return element;
|
|
if (raw == nullptr) return read_index(*memory, base, type, element);
|
|
if (type == 1u) return raw[element];
|
|
if (type == 2u) {
|
|
const std::size_t o = static_cast<std::size_t>(element) * 2u;
|
|
return static_cast<std::uint32_t>(raw[o]) |
|
|
(static_cast<std::uint32_t>(raw[o + 1u]) << 8u);
|
|
}
|
|
const std::size_t o = static_cast<std::size_t>(element) * 4u;
|
|
return static_cast<std::uint32_t>(raw[o]) |
|
|
(static_cast<std::uint32_t>(raw[o + 1u]) << 8u) |
|
|
(static_cast<std::uint32_t>(raw[o + 2u]) << 16u) |
|
|
(static_cast<std::uint32_t>(raw[o + 3u]) << 24u);
|
|
}
|
|
};
|
|
|
|
IndexStreamReader make_index_reader(const psprecomp::GuestMemory &memory,
|
|
std::uint32_t base, std::uint32_t index_type,
|
|
std::uint32_t count) noexcept {
|
|
std::size_t bytes = 0u;
|
|
if (index_type >= 1u && index_type <= 3u) {
|
|
const std::size_t width = index_type == 1u ? 1u : index_type == 2u ? 2u : 4u;
|
|
if (count <= std::numeric_limits<std::size_t>::max() / width)
|
|
bytes = static_cast<std::size_t>(count) * width;
|
|
}
|
|
return IndexStreamReader{&memory, bytes != 0u ? memory.raw_pointer(base, bytes) : nullptr,
|
|
base, index_type};
|
|
}
|
|
|
|
std::uint32_t index_size(std::uint32_t index_type) noexcept {
|
|
switch (index_type) { case 1u: return 1u; case 2u: return 2u; case 3u: return 4u; default: return 0u; }
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool test_ge_bounding_box(const psprecomp::GuestMemory &memory,
|
|
const std::array<std::uint32_t, 256> &commands,
|
|
const GeTransformState &transform,
|
|
std::uint32_t vertex_address,
|
|
std::uint32_t index_address,
|
|
std::uint32_t count,
|
|
GeBoundingBoxResult &result,
|
|
std::string &error) {
|
|
result = GeBoundingBoxResult{false, vertex_address, index_address};
|
|
if (count == 0u) return true;
|
|
|
|
VertexLayout layout{};
|
|
if (!build_vertex_layout_cached(data24(commands[0x12u]), layout, error)) return false;
|
|
const std::uint32_t isize = index_size(layout.index_type);
|
|
if (isize == 0u)
|
|
result.next_vertex_address = vertex_address + count * layout.stride;
|
|
else
|
|
result.next_index_address = index_address + count * isize;
|
|
|
|
if (isize != 0u && !memory.contains(index_address, static_cast<std::size_t>(count) * isize)) {
|
|
error = "GE BBOX index stream lies outside guest memory";
|
|
return false;
|
|
}
|
|
|
|
const bool depth_clip_enabled = (data24(commands[0x1Cu]) & 1u) != 0u;
|
|
const std::uint32_t plane_count = depth_clip_enabled ? 6u : 4u;
|
|
std::array<bool, 6> all_outside{};
|
|
all_outside.fill(true);
|
|
|
|
bool through_visible = false;
|
|
const IndexStreamReader bbox_indices =
|
|
make_index_reader(memory, index_address, layout.index_type, count);
|
|
for (std::uint32_t element = 0u; element < count; ++element) {
|
|
const std::uint32_t index = bbox_indices(element);
|
|
Vertex vertex{};
|
|
if (!decode_vertex(memory, vertex_address + index * layout.stride, layout,
|
|
commands, transform, vertex, error)) return false;
|
|
|
|
if (layout.through) {
|
|
const std::uint32_t region1 = data24(commands[0x15u]);
|
|
const std::uint32_t region2 = data24(commands[0x16u]);
|
|
const float x1 = static_cast<float>(region1 & 0x3FFu);
|
|
const float y1 = static_cast<float>((region1 >> 10u) & 0x3FFu);
|
|
const float x2 = static_cast<float>(region2 == 0u ? 1023u : region2 & 0x3FFu);
|
|
const float y2 = static_cast<float>(region2 == 0u ? 1023u : (region2 >> 10u) & 0x3FFu);
|
|
through_visible = through_visible ||
|
|
(vertex.x >= x1 && vertex.x <= x2 && vertex.y >= y1 && vertex.y <= y2);
|
|
continue;
|
|
}
|
|
|
|
for (std::uint32_t plane = 0u; plane < plane_count; ++plane) {
|
|
if (clip_distance(vertex, plane) >= 0.0f) all_outside[plane] = false;
|
|
}
|
|
}
|
|
|
|
if (layout.through) {
|
|
result.visible = through_visible;
|
|
} else {
|
|
result.visible = true;
|
|
for (std::uint32_t plane = 0u; plane < plane_count; ++plane) {
|
|
if (all_outside[plane]) {
|
|
result.visible = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool render_ge_primitive(psprecomp::GuestMemory &memory,
|
|
const std::array<std::uint32_t, 256> &commands,
|
|
const GeTransformState &transform,
|
|
std::uint32_t vertex_address,
|
|
std::uint32_t index_address,
|
|
std::uint32_t primitive_data,
|
|
GeRenderStats &stats,
|
|
std::string &error,
|
|
std::uint32_t logical_primitive_count,
|
|
std::uint64_t draw_state_revision,
|
|
std::uint64_t camera_state_revision,
|
|
std::uint64_t lighting_state_revision,
|
|
bool collect_diagnostic_stats) {
|
|
GeRenderStatsCollectionScope stats_scope(collect_diagnostic_stats);
|
|
stats.next_vertex_address = vertex_address;
|
|
stats.next_index_address = index_address;
|
|
if (collect_diagnostic_stats) ++stats.primitives;
|
|
const std::uint32_t primitive = (primitive_data >> 16u) & 7u;
|
|
const std::uint32_t count = primitive_data & 0xFFFFu;
|
|
if (count == 0u) return true;
|
|
|
|
VertexLayout layout;
|
|
if (!build_vertex_layout_cached(data24(commands[0x12u]), layout, error)) return false;
|
|
|
|
if (ge_phase_diag_enabled()) {
|
|
++g_ge_primitive_count;
|
|
g_ge_vertex_count += count;
|
|
}
|
|
|
|
const bool gpu_backend_enabled = ge_gpu_backend_active();
|
|
GeGpuDrawDescriptor gpu_draw{};
|
|
{
|
|
PhaseTimer draw_setup_timer(g_ge_draw_setup_ns);
|
|
if (gpu_backend_enabled) {
|
|
// Cached GE state reuse: most adjacent PRIM commands only
|
|
// advance vertex/index pointers while texture, blend, depth, scissor and
|
|
// framebuffer state stay unchanged. Re-decoding ~60 GE registers and all
|
|
// eight mip descriptors for every draw was pure host overhead. The GE
|
|
// interpreter supplies a monotonic revision that excludes VADDR/IADDR/PRIM.
|
|
static thread_local std::uint64_t cached_draw_revision =
|
|
std::numeric_limits<std::uint64_t>::max();
|
|
static thread_local GeGpuDrawDescriptor cached_draw_state{};
|
|
const bool reuse_draw_state = draw_state_revision != 0u &&
|
|
cached_draw_revision == draw_state_revision;
|
|
if (reuse_draw_state) {
|
|
gpu_draw = cached_draw_state;
|
|
} else {
|
|
gpu_draw.vertex_type = data24(commands[0x12u]);
|
|
gpu_draw.framebuffer_address = framebuffer_address(commands);
|
|
gpu_draw.framebuffer_stride = data24(commands[0x9Du]) & 0x7FCu;
|
|
gpu_draw.framebuffer_format = data24(commands[0xD2u]) & 3u;
|
|
gpu_draw.texture_format = data24(commands[0xC3u]) & 0xFu;
|
|
gpu_draw.texture_selected_level=selected_texture_level(commands);
|
|
for(std::uint32_t level=0;level<8;++level){gpu_draw.texture_level_addresses[level]=texture_address(commands,level);gpu_draw.texture_level_buffer_widths[level]=std::max<std::uint32_t>(1u,data24(commands[0xA8u+level])&0x7FFu);const auto sz=data24(commands[0xB8u+level]);gpu_draw.texture_level_widths[level]=1u<<(sz&0xFu);gpu_draw.texture_level_heights[level]=1u<<((sz>>8u)&0xFu);}
|
|
gpu_draw.texture_address=gpu_draw.texture_level_addresses[0];gpu_draw.texture_buffer_width=gpu_draw.texture_level_buffer_widths[0];gpu_draw.texture_width=gpu_draw.texture_level_widths[0];gpu_draw.texture_height=gpu_draw.texture_level_heights[0];
|
|
const std::uint32_t gpu_texfunc = data24(commands[0xC9u]);
|
|
gpu_draw.texture_function = gpu_texfunc & 7u;
|
|
gpu_draw.texture_use_alpha = (gpu_texfunc & 0x100u) != 0u;
|
|
gpu_draw.texture_double_color = (gpu_texfunc & 0x10000u) != 0u;
|
|
gpu_draw.texture_env = data24(commands[0xCAu]) & 0x00FFFFFFu;
|
|
gpu_draw.clut_address = clut_address(commands);
|
|
const std::uint32_t clut_data = data24(commands[0xC5u]);
|
|
gpu_draw.clut_format = clut_data & 3u;
|
|
gpu_draw.clut_shift = (clut_data >> 2u) & 0x1Fu;
|
|
gpu_draw.clut_mask = (clut_data >> 8u) & 0xFFu;
|
|
gpu_draw.clut_start = ((clut_data >> 16u) & 0x1Fu) << 4u;
|
|
const std::uint32_t texture_mode = data24(commands[0xC2u]);
|
|
const std::uint32_t texture_filter = data24(commands[0xC6u]);
|
|
const std::uint32_t texture_level = data24(commands[0xC8u]);
|
|
gpu_draw.texture_swizzled = (texture_mode & 1u) != 0u;
|
|
gpu_draw.texture_min_linear = (texture_filter & 1u) != 0u;
|
|
gpu_draw.texture_mipmap_linear = (texture_filter & 2u) != 0u;
|
|
gpu_draw.texture_mipmap_enabled = (texture_filter & 4u) != 0u;
|
|
gpu_draw.texture_mag_linear = ((texture_filter >> 8u) & 1u) != 0u;
|
|
gpu_draw.texture_linear = gpu_draw.texture_mag_linear;
|
|
gpu_draw.texture_max_level = (texture_mode >> 16u) & 7u;
|
|
gpu_draw.texture_level_mode = texture_level & 3u;
|
|
gpu_draw.texture_level_offset16 = signed_texture_lod_offset16(texture_level);
|
|
gpu_draw.texture_lod_slope = decode_float24(data24(commands[0xD0u]));
|
|
const std::uint32_t texture_wrap = data24(commands[0xC7u]);
|
|
gpu_draw.texture_clamp_u = (texture_wrap & 1u) != 0u;
|
|
gpu_draw.texture_clamp_v = (texture_wrap & 0x100u) != 0u;
|
|
// Same decode as make_fragment_setup(); the GPU path needs it too.
|
|
const std::uint32_t gpu_scissor1 = data24(commands[0xD4u]);
|
|
const std::uint32_t gpu_scissor2 = data24(commands[0xD5u]);
|
|
gpu_draw.scissor_x0 = static_cast<std::int32_t>(gpu_scissor1 & 0x3FFu);
|
|
gpu_draw.scissor_y0 = static_cast<std::int32_t>((gpu_scissor1 >> 10u) & 0x3FFu);
|
|
gpu_draw.scissor_x1 = gpu_scissor2 == 0u
|
|
? static_cast<std::int32_t>(gpu_draw.framebuffer_stride - 1u)
|
|
: static_cast<std::int32_t>(gpu_scissor2 & 0x3FFu);
|
|
gpu_draw.scissor_y1 = gpu_scissor2 == 0u
|
|
? 271 : static_cast<std::int32_t>((gpu_scissor2 >> 10u) & 0x3FFu);
|
|
gpu_draw.through = layout.through;
|
|
gpu_draw.texture_enabled = (data24(commands[0x1Eu]) & 1u) != 0u;
|
|
gpu_draw.blend_enabled = (data24(commands[0x21u]) & 1u) != 0u;
|
|
const std::uint32_t blend_mode = data24(commands[0xDFu]);
|
|
gpu_draw.blend_source_factor = blend_mode & 0xFu;
|
|
gpu_draw.blend_dest_factor = (blend_mode >> 4u) & 0xFu;
|
|
gpu_draw.blend_equation = (blend_mode >> 8u) & 7u;
|
|
gpu_draw.blend_fix_source = data24(commands[0xE0u]) & 0x00FFFFFFu;
|
|
gpu_draw.blend_fix_dest = data24(commands[0xE1u]) & 0x00FFFFFFu;
|
|
gpu_draw.color_write_mask = data24(commands[0xE8u]) |
|
|
((data24(commands[0xE9u]) & 0xFFu) << 24u);
|
|
const std::uint32_t alpha_test = data24(commands[0xDBu]);
|
|
gpu_draw.alpha_test_enabled = (data24(commands[0x22u]) & 1u) != 0u;
|
|
gpu_draw.alpha_function = alpha_test & 7u;
|
|
gpu_draw.alpha_reference = (alpha_test >> 8u) & 0xFFu;
|
|
gpu_draw.alpha_mask = (alpha_test >> 16u) & 0xFFu;
|
|
gpu_draw.depth_test_enabled = (data24(commands[0x23u]) & 1u) != 0u;
|
|
gpu_draw.depth_write_enabled = (data24(commands[0xE7u]) & 1u) == 0u;
|
|
gpu_draw.depth_function = data24(commands[0xDEu]) & 7u;
|
|
gpu_draw.fog_enabled = (data24(commands[0x1Fu]) & 1u) != 0u;
|
|
gpu_draw.fog_color = data24(commands[0xCFu]) & 0x00FFFFFFu;
|
|
gpu_draw.fog_end = decode_float24(data24(commands[0xCDu]));
|
|
gpu_draw.fog_slope = decode_float24(data24(commands[0xCEu]));
|
|
const std::uint32_t gpu_clear = data24(commands[0xD3u]);
|
|
gpu_draw.clear_mode = (gpu_clear & 1u) != 0u;
|
|
gpu_draw.clear_color = (gpu_clear & 0x100u) != 0u;
|
|
gpu_draw.clear_alpha = (gpu_clear & 0x200u) != 0u;
|
|
gpu_draw.clear_depth = (gpu_clear & 0x400u) != 0u;
|
|
gpu_draw.texture_content_signature = 0u;
|
|
gpu_draw.clut_checksum = 0u;
|
|
if (draw_state_revision != 0u) {
|
|
cached_draw_state = gpu_draw;
|
|
cached_draw_revision = draw_state_revision;
|
|
}
|
|
}
|
|
gpu_draw.primitive = primitive;
|
|
gpu_draw.vertex_count = count;
|
|
|
|
// CLUT bytes can be rewritten by guest code between lists without a GE
|
|
// register change, so preserve conservative per-draw palette validation.
|
|
gpu_draw.clut_checksum = 0u;
|
|
if (gpu_draw.texture_format >= 4u && gpu_draw.texture_format <= 7u &&
|
|
gpu_draw.clut_address != 0u) {
|
|
// Only the window this draw can index: index = ((raw >> shift) &
|
|
// mask) + start. Hashing all 256 entries folded in palette slots the
|
|
// texture never reads, so an unrelated recolour elsewhere in the CLUT
|
|
// minted a new cache key -- 35491 shared images and 850 MB decoded in
|
|
// one session, against 587 and 75 MB.
|
|
const std::uint32_t entry_bytes = gpu_draw.clut_format == 3u ? 4u : 2u;
|
|
const std::uint32_t first = std::min(gpu_draw.clut_start, 255u);
|
|
const std::uint32_t last = std::min(first + gpu_draw.clut_mask, 255u);
|
|
const std::uint32_t offset_bytes = first * entry_bytes;
|
|
const std::uint32_t clut_bytes = (last - first + 1u) * entry_bytes;
|
|
if (const std::uint8_t *clut = memory.raw_pointer(
|
|
gpu_draw.clut_address + offset_bytes, clut_bytes)) {
|
|
std::uint32_t checksum = 2166136261u;
|
|
std::uint32_t offset = 0u;
|
|
for (; offset + 4u <= clut_bytes; offset += 4u) {
|
|
const std::uint32_t word = static_cast<std::uint32_t>(clut[offset]) |
|
|
(static_cast<std::uint32_t>(clut[offset + 1u]) << 8u) |
|
|
(static_cast<std::uint32_t>(clut[offset + 2u]) << 16u) |
|
|
(static_cast<std::uint32_t>(clut[offset + 3u]) << 24u);
|
|
checksum ^= word;
|
|
checksum *= 16777619u;
|
|
}
|
|
if (offset < clut_bytes) {
|
|
std::uint32_t tail = clut[offset];
|
|
if (offset + 1u < clut_bytes) tail |= static_cast<std::uint32_t>(clut[offset + 1u]) << 8u;
|
|
checksum ^= tail;
|
|
checksum *= 16777619u;
|
|
}
|
|
gpu_draw.clut_checksum = checksum;
|
|
}
|
|
}
|
|
// All texture identity fields, including the mutable CLUT checksum, are
|
|
// final now. Hash them once; the backend will reuse these derived keys
|
|
// throughout signature/cache/upload/accumulation for this PRIM.
|
|
ge_gpu_backend_prepare_texture_keys(gpu_draw);
|
|
gpu_draw.texture_content_signature = 0u;
|
|
if (gpu_draw.texture_enabled && ge_gpu_backend_texture_signature_needed(gpu_draw)) {
|
|
const bool feedback = ge_gpu_backend_is_framebuffer_feedback_texture(gpu_draw);
|
|
const std::uint32_t signature_levels = feedback ? 1u :
|
|
(gpu_draw.texture_mipmap_enabled
|
|
? std::min<std::uint32_t>(8u, gpu_draw.texture_max_level + 1u) : 1u);
|
|
std::uint64_t signature = 0xCBF29CE484222325ull;
|
|
bool any_signature = false;
|
|
for (std::uint32_t level = 0u; level < signature_levels; ++level) {
|
|
const TextureSetup source = make_texture_setup_for_level(memory, commands, level);
|
|
const std::uint64_t part = texture_source_signature(memory, source);
|
|
if (part == 0u) continue;
|
|
any_signature = true;
|
|
signature ^= part + 0x9E3779B97F4A7C15ull + (signature << 6u) + (signature >> 2u);
|
|
}
|
|
gpu_draw.texture_content_signature = any_signature ? signature : 0u;
|
|
}
|
|
ge_gpu_backend_record_draw(gpu_draw);
|
|
if (!gpu_draw.through && !gpu_draw.clear_mode &&
|
|
primitive >= 3u && primitive <= 5u) {
|
|
const std::array<float, 6> cloud_viewport{
|
|
decode_float24(data24(commands[0x42u])),
|
|
decode_float24(data24(commands[0x43u])),
|
|
decode_float24(data24(commands[0x45u])),
|
|
decode_float24(data24(commands[0x46u])),
|
|
static_cast<float>(data24(commands[0x4Cu]) & 0xFFFFu) / 16.0f,
|
|
static_cast<float>(data24(commands[0x4Du]) & 0xFFFFu) / 16.0f};
|
|
// VCS' authoritative camera origin. The affine GE view used by
|
|
// individual passes is not guaranteed to encode this position as
|
|
// a rigid inverse (reflections and camera-relative passes do not),
|
|
// which made a world-space cloud slab orbit while only turning.
|
|
constexpr std::uint32_t kVcsCameraPosition = 0x08BC87E0u;
|
|
std::array<float, 3> cloud_camera_position{};
|
|
if (memory.contains(kVcsCameraPosition, 12u)) {
|
|
cloud_camera_position = {
|
|
std::bit_cast<float>(memory.load32(kVcsCameraPosition + 0u)),
|
|
std::bit_cast<float>(memory.load32(kVcsCameraPosition + 4u)),
|
|
std::bit_cast<float>(memory.load32(kVcsCameraPosition + 8u))};
|
|
}
|
|
ge_gpu_backend_observe_camera(transform.view, transform.projection,
|
|
cloud_viewport, cloud_camera_position,
|
|
gpu_draw, count);
|
|
}
|
|
if (!gpu_draw.clear_mode && primitive >= 3u && primitive <= 6u)
|
|
fps_overlay_observe_draw(gpu_draw, count);
|
|
if (!gpu_draw.through && !gpu_draw.clear_mode &&
|
|
primitive >= 3u && primitive <= 5u &&
|
|
!project2dfx_observe_camera_hot(gpu_draw, count, camera_state_revision)) {
|
|
project2dfx_observe_camera(
|
|
transform.view, transform.projection,
|
|
decode_float24(data24(commands[0x42u])),
|
|
decode_float24(data24(commands[0x43u])),
|
|
decode_float24(data24(commands[0x44u])),
|
|
decode_float24(data24(commands[0x45u])),
|
|
decode_float24(data24(commands[0x46u])),
|
|
decode_float24(data24(commands[0x47u])),
|
|
static_cast<float>(data24(commands[0x4Cu]) & 0xFFFFu) / 16.0f,
|
|
static_cast<float>(data24(commands[0x4Du]) & 0xFFFFu) / 16.0f,
|
|
1.0f, gpu_draw, count, camera_state_revision);
|
|
}
|
|
}
|
|
}
|
|
const std::uint32_t isize = index_size(layout.index_type);
|
|
auto advance_stream = [&]() {
|
|
if (isize == 0u) stats.next_vertex_address = vertex_address + count * layout.stride;
|
|
else stats.next_index_address = index_address + count * isize;
|
|
};
|
|
if (isize != 0u && !memory.contains(index_address, static_cast<std::size_t>(count) * isize)) {
|
|
error = "GE index stream lies outside guest memory";
|
|
return false;
|
|
}
|
|
|
|
// Rectangle primitives are a through-mode construct. Unknown primitive families
|
|
// still advance the stream so malformed or future lists do not desynchronize it.
|
|
if (primitive > 6u || (!layout.through && primitive == 6u)) {
|
|
if (collect_diagnostic_stats) ++stats.unsupported_primitives;
|
|
advance_stream();
|
|
error.clear();
|
|
return true;
|
|
}
|
|
|
|
if (collect_diagnostic_stats && layout.weight_type != 0u) stats.skinned_vertices += count;
|
|
if (collect_diagnostic_stats && layout.morph_count > 1u) stats.morphed_vertices += count;
|
|
if (collect_diagnostic_stats && !layout.through && (data24(commands[0x17u]) & 1u) != 0u) stats.lit_vertices += count;
|
|
const std::uint32_t uv_generation = data24(commands[0xC0u]) & 3u;
|
|
if (!layout.through && (uv_generation == 1u || uv_generation == 2u))
|
|
if (collect_diagnostic_stats) stats.generated_uv_vertices += count;
|
|
|
|
// Decide the pure Vulkan path before resolving software-raster state. The
|
|
// Stage 39 hardware-transform path returns before touching FragmentSetup,
|
|
// so decoding framebuffer/depth pointers and CPU fragment state here was
|
|
// dead work on the dominant city draw path.
|
|
const bool gpu_only_triangle_path = gpu_backend_enabled && software_raster_skipped(commands);
|
|
|
|
// Decode the complete PSP mip chain into one contiguous allocation. The
|
|
// old vector-per-level path then copied the entire chain a second time in
|
|
// ge_gpu_backend before Vulkan could see it. Streaming bursts are exactly
|
|
// where that extra allocation/copy hurts most.
|
|
if (gpu_backend_enabled && gpu_draw.texture_enabled &&
|
|
ge_gpu_backend_texture_needed(gpu_draw) &&
|
|
!ge_gpu_backend_adopt_shared_texture(gpu_draw)) {
|
|
PhaseTimer texture_timer(g_ge_texture_upload_ns);
|
|
const bool framebuffer_feedback =
|
|
ge_gpu_backend_is_framebuffer_feedback_texture(gpu_draw);
|
|
const std::uint32_t level_count = framebuffer_feedback ? 1u :
|
|
(gpu_draw.texture_mipmap_enabled
|
|
? std::min<std::uint32_t>(8u, gpu_draw.texture_max_level + 1u) : 1u);
|
|
std::array<TextureSetup, 8> mip_setups{};
|
|
std::size_t total_bytes = 0u;
|
|
bool all = true;
|
|
for (std::uint32_t level = 0u; level < level_count; ++level) {
|
|
mip_setups[level] = make_texture_setup_for_level(memory, commands, level);
|
|
const std::uint64_t bytes = static_cast<std::uint64_t>(mip_setups[level].width) *
|
|
mip_setups[level].height * 4ull;
|
|
if (bytes > std::numeric_limits<std::size_t>::max() - total_bytes) {
|
|
all = false;
|
|
break;
|
|
}
|
|
total_bytes += static_cast<std::size_t>(bytes);
|
|
}
|
|
std::vector<std::byte> decoded;
|
|
if (all) {
|
|
try { decoded.resize(total_bytes); } catch (...) { all = false; }
|
|
}
|
|
std::size_t offset = 0u;
|
|
for (std::uint32_t level = 0u; all && level < level_count; ++level) {
|
|
const std::size_t bytes = static_cast<std::size_t>(mip_setups[level].width) *
|
|
mip_setups[level].height * 4u;
|
|
if (!decode_texture_rgba_into(memory, mip_setups[level],
|
|
std::span<std::byte>(decoded).subspan(offset, bytes))) {
|
|
all = false;
|
|
break;
|
|
}
|
|
offset += bytes;
|
|
}
|
|
if (all) {
|
|
(void)ge_gpu_backend_upload_decoded_texture_chain_packed(
|
|
gpu_draw, mip_setups[0].width, mip_setups[0].height,
|
|
level_count, std::move(decoded));
|
|
} else {
|
|
(void)ge_gpu_backend_upload_decoded_texture(gpu_draw, 0u, 0u, {});
|
|
}
|
|
}
|
|
|
|
// Hardware-transform frontend. The CPU still decodes
|
|
// PSP vertex formats (and skin/morph data when present), but projected
|
|
// triangle draws no longer materialize clip polygons or screen-space
|
|
// PreparedScreenTriangle objects. World/view/projection, viewport mapping,
|
|
// clipping and rasterization stay on Vulkan.
|
|
// Lit draws are admitted through the hybrid path: the CPU applies the world
|
|
// matrix and the lighting, the GPU still does view/projection/viewport/clip.
|
|
// Refusing them outright left 90.5% of city vertices on the legacy path.
|
|
//
|
|
// A normal is required, because that is what apply_lighting reads.
|
|
// PSPRECOMP_GE_GPU_HW_LIT=0 restores the Stage 39 behaviour of refusing
|
|
// every lit draw, for A/B in one binary.
|
|
static const bool hw_lit_enabled = [] {
|
|
const char *text = std::getenv("PSPRECOMP_GE_GPU_HW_LIT");
|
|
return text == nullptr || (*text != '\0' && std::strcmp(text, "0") != 0);
|
|
}();
|
|
const bool lighting_on = (data24(commands[0x17u]) & 1u) != 0u;
|
|
// A missing normal is not a reason to refuse: decode_vertex() defaults it to
|
|
// {0,0,1} and lights the vertex anyway, so the hybrid does the same. Only
|
|
// morph targets stay out, because their normals are blended per target and
|
|
// that path is not worth replicating for the volume involved.
|
|
const bool hw_lighting_on_cpu = lighting_on && hw_lit_enabled &&
|
|
layout.morph_count == 1u;
|
|
const bool hw_lighting_compatible = !lighting_on || hw_lighting_on_cpu;
|
|
|
|
const bool hw_transform_eligible = gpu_only_triangle_path &&
|
|
ge_gpu_backend_graphics_ready() && gpu_hardware_transform_enabled() &&
|
|
!layout.through && primitive >= 3u && primitive <= 5u &&
|
|
!gpu_draw.clear_mode && hw_lighting_compatible &&
|
|
layout.position_type != 0u;
|
|
|
|
if (hw_transform_eligible) {
|
|
PreparedLighting prepared_lighting{};
|
|
if (hw_lighting_on_cpu) {
|
|
// Lighting registers normally stay unchanged for long runs of world draws.
|
|
// Decoding/normalizing all four PSP lights per PRIM was pure serial GE work.
|
|
// The profile supplies a monotonic lighting-state revision, so reuse the
|
|
// prepared immutable state until one of the actual lighting/material
|
|
// registers changes. Tests/standalone callers pass revision 0 and keep
|
|
// the exact uncached behavior.
|
|
struct LightingCache {
|
|
std::uint64_t revision{};
|
|
bool has_vertex_color{};
|
|
bool valid{};
|
|
PreparedLighting state{};
|
|
};
|
|
static thread_local LightingCache lighting_cache;
|
|
const bool has_vertex_color = layout.color_type >= 4u;
|
|
if (lighting_state_revision != 0u && lighting_cache.valid &&
|
|
lighting_cache.revision == lighting_state_revision &&
|
|
lighting_cache.has_vertex_color == has_vertex_color) {
|
|
prepared_lighting = lighting_cache.state;
|
|
} else {
|
|
prepared_lighting = prepare_lighting(has_vertex_color, commands);
|
|
if (lighting_state_revision != 0u) {
|
|
lighting_cache.revision = lighting_state_revision;
|
|
lighting_cache.has_vertex_color = has_vertex_color;
|
|
lighting_cache.state = prepared_lighting;
|
|
lighting_cache.valid = true;
|
|
}
|
|
}
|
|
}
|
|
static thread_local std::vector<std::uint32_t> occurrence_indices;
|
|
static thread_local std::vector<std::uint32_t> unique_indices;
|
|
static thread_local std::vector<std::uint32_t> occurrence_remap;
|
|
static thread_local std::vector<GeGpuVertex> decoded_vertices;
|
|
static thread_local std::vector<GeGpuVertex> submitted_vertices;
|
|
static thread_local std::vector<std::uint32_t> triangle_indices;
|
|
occurrence_indices.clear();
|
|
unique_indices.clear();
|
|
occurrence_remap.clear();
|
|
decoded_vertices.clear();
|
|
submitted_vertices.clear();
|
|
triangle_indices.clear();
|
|
|
|
// Common VCS meshes are either non-indexed or index a compact contiguous
|
|
// vertex range. Do not manufacture 0..N identity vectors (nor a second
|
|
// remap vector) for those cases. Sparse streams keep the conservative
|
|
// sorted-unique fallback.
|
|
const bool indexed = isize != 0u;
|
|
bool contiguous_decode = !indexed;
|
|
bool sequential_index_stream = indexed;
|
|
std::uint32_t first_index_value = 0u;
|
|
std::uint32_t contiguous_first = 0u;
|
|
std::uint32_t contiguous_count = indexed ? 0u : count;
|
|
|
|
{
|
|
PhaseTimer vertex_timer(g_ge_vertex_decode_ns);
|
|
if (indexed) {
|
|
occurrence_indices.reserve(count);
|
|
const IndexStreamReader draw_indices =
|
|
make_index_reader(memory, index_address, layout.index_type, count);
|
|
std::uint32_t lower = std::numeric_limits<std::uint32_t>::max();
|
|
std::uint32_t upper = 0u;
|
|
for (std::uint32_t i = 0u; i < count; ++i) {
|
|
const std::uint32_t index = draw_indices(i);
|
|
occurrence_indices.push_back(index);
|
|
if (i == 0u) {
|
|
first_index_value = index;
|
|
} else if (index != first_index_value + i) {
|
|
sequential_index_stream = false;
|
|
}
|
|
lower = std::min(lower, index);
|
|
upper = std::max(upper, index);
|
|
}
|
|
const std::uint64_t range64 = count == 0u ? 0u
|
|
: static_cast<std::uint64_t>(upper) - lower + 1u;
|
|
const bool compact_range = range64 != 0u && range64 <= 65536u &&
|
|
range64 <= std::max<std::uint64_t>(64u, static_cast<std::uint64_t>(count) * 2u);
|
|
if (compact_range) {
|
|
contiguous_decode = true;
|
|
contiguous_first = lower;
|
|
contiguous_count = static_cast<std::uint32_t>(range64);
|
|
} else {
|
|
unique_indices = occurrence_indices;
|
|
std::sort(unique_indices.begin(), unique_indices.end());
|
|
unique_indices.erase(std::unique(unique_indices.begin(), unique_indices.end()),
|
|
unique_indices.end());
|
|
occurrence_remap.reserve(count);
|
|
for (std::uint32_t index : occurrence_indices) {
|
|
const auto found = std::lower_bound(unique_indices.begin(), unique_indices.end(), index);
|
|
occurrence_remap.push_back(static_cast<std::uint32_t>(found - unique_indices.begin()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const std::size_t decode_count = contiguous_decode
|
|
? static_cast<std::size_t>(contiguous_count) : unique_indices.size();
|
|
|
|
// Stage 45.4 city fast span. The overwhelmingly common 0x0115 layout
|
|
// used to call GuestMemory::raw_pointer() once for every decoded vertex.
|
|
// A compact/non-indexed draw is contiguous by construction, so validate
|
|
// the full byte range once. The exact same span can now bypass CPU
|
|
// conversion entirely on DX12 and be decoded by the vertex shader.
|
|
const bool fast_0115_layout = layout.type == 0x000115u && !layout.through &&
|
|
layout.morph_count == 1u && layout.weight_type == 0u &&
|
|
layout.normal_type == 0u && uv_generation == 0u && layout.stride == 10u;
|
|
// Stage 45.5 validates the compact source range once for projected draws.
|
|
// The dominant 0x0115 path can then hand the untouched PSP bytes to DX12
|
|
// without any per-vertex GuestMemory calls.
|
|
const std::uint8_t *contiguous_raw = nullptr;
|
|
std::size_t contiguous_raw_bytes = 0u;
|
|
if (contiguous_decode && decode_count != 0u) {
|
|
const std::uint64_t first_address64 = static_cast<std::uint64_t>(vertex_address) +
|
|
static_cast<std::uint64_t>(contiguous_first) * layout.stride;
|
|
const std::uint64_t byte_count64 = static_cast<std::uint64_t>(decode_count) * layout.stride;
|
|
if (first_address64 <= std::numeric_limits<std::uint32_t>::max() &&
|
|
byte_count64 <= std::numeric_limits<std::size_t>::max()) {
|
|
contiguous_raw_bytes = static_cast<std::size_t>(byte_count64);
|
|
contiguous_raw = memory.raw_pointer(
|
|
static_cast<std::uint32_t>(first_address64), contiguous_raw_bytes);
|
|
}
|
|
}
|
|
const std::uint8_t *fast_0115_raw = fast_0115_layout ? contiguous_raw : nullptr;
|
|
const std::size_t fast_0115_bytes = fast_0115_raw != nullptr ? contiguous_raw_bytes : 0u;
|
|
Vec3 fast_0115_world_normal{};
|
|
const Vec3 *fast_0115_world_normal_ptr = nullptr;
|
|
if (fast_0115_raw != nullptr && hw_lighting_on_cpu) {
|
|
fast_0115_world_normal = transform_normal_4x3(
|
|
transform.world, Vec3{0.0f, 0.0f, 1.0f});
|
|
if ((data24(commands[0x51u]) & 1u) != 0u)
|
|
fast_0115_world_normal = fast_0115_world_normal * -1.0f;
|
|
fast_0115_world_normal = normalized_or_001(fast_0115_world_normal);
|
|
fast_0115_world_normal_ptr = &fast_0115_world_normal;
|
|
}
|
|
|
|
const GeGpuDrawDescriptor effective_draw = gpu_effective_draw_descriptor(gpu_draw);
|
|
const bool sampled_texture_ready = effective_draw.texture_enabled &&
|
|
ge_gpu_backend_texture_available(effective_draw);
|
|
|
|
// Directional-only lighting on 0x0115 is affine in vertex colour and
|
|
// can be performed exactly once in the VS, so the draw no longer needs
|
|
// CPU world transform + lighting for every vertex. Keep diagnostic/
|
|
// missing-texture colour overrides on the legacy CPU path so this fast
|
|
// path never changes their intended output.
|
|
std::array<float, 4> gpu_light_mul{1.0f, 1.0f, 1.0f, 1.0f};
|
|
std::array<float, 4> gpu_light_add{};
|
|
bool gpu_directional_lighting = false;
|
|
if (fast_0115_raw != nullptr && hw_lighting_on_cpu &&
|
|
fast_0115_world_normal_ptr != nullptr &&
|
|
!gpu_force_white_vertex_colors_enabled() && !gpu_geometry_debug_colors_enabled() &&
|
|
(!effective_draw.texture_enabled || sampled_texture_ready)) {
|
|
gpu_directional_lighting = prepare_directional_lighting_affine(
|
|
prepared_lighting, *fast_0115_world_normal_ptr, gpu_light_mul, gpu_light_add);
|
|
}
|
|
const bool cpu_lighting_effective = hw_lighting_on_cpu && !gpu_directional_lighting;
|
|
const bool flat_shading = (data24(commands[0x50u]) & 1u) == 0u;
|
|
GeGpuHardwareTransform hw =
|
|
build_gpu_hardware_transform(commands, transform, cpu_lighting_effective,
|
|
uv_generation == 1u || uv_generation == 2u);
|
|
// D3D12 supports strips natively; triangle fans are expanded to a list.
|
|
hw.primitive = primitive == 5u ? 3u : primitive;
|
|
hw.vertex_color_affine = gpu_directional_lighting;
|
|
hw.vertex_color_mul = gpu_light_mul;
|
|
hw.vertex_color_add = gpu_light_add;
|
|
hw.logical_prim_batches = std::max<std::uint32_t>(1u, logical_primitive_count);
|
|
hw.unique_vertices_decoded = static_cast<std::uint32_t>(decode_count);
|
|
hw.index_reuses = count > decode_count
|
|
? count - static_cast<std::uint32_t>(decode_count) : 0u;
|
|
|
|
// The biggest 45.4 GE optimization: for the common unlit/smooth 0x0115
|
|
// world stream, do not manufacture a 56-byte GeGpuVertex (or even the
|
|
// compact 36-byte DX12 vertex) at all. Snapshot the original 10-byte PSP
|
|
// records and let the DX12 VS unpack them. This removes format conversion,
|
|
// color expansion and s16->float normalization from the serialized GE
|
|
// thread while cutting upload traffic by another 72% versus the new 36B
|
|
// stream (82% versus the original 56B stream).
|
|
const bool packed_0115_candidate = packed_0115_gpu_decode_enabled() &&
|
|
fast_0115_raw != nullptr && !cpu_lighting_effective && !flat_shading &&
|
|
!gpu_force_white_vertex_colors_enabled() && !gpu_geometry_debug_colors_enabled() &&
|
|
(!effective_draw.texture_enabled || sampled_texture_ready);
|
|
if (packed_0115_candidate) {
|
|
const auto occurrence_index = [&](std::size_t i) -> std::uint32_t {
|
|
if (!indexed) return static_cast<std::uint32_t>(i);
|
|
if (contiguous_decode) return occurrence_indices[i] - contiguous_first;
|
|
return occurrence_remap[i];
|
|
};
|
|
bool needs_indices = true;
|
|
// Stable DrawInstanced fast path. Non-indexed triangle lists were
|
|
// already direct; indexed lists that are literally N,N+1,N+2...
|
|
// are semantically identical and can drop their index stream too.
|
|
// This keeps the 44.7 non-indexed D3D12 submission semantics while
|
|
// avoiding both triangle-index construction and 10-byte vertex
|
|
// expansion for identity-index meshes.
|
|
const bool identity_indexed_triangle_list = indexed && sequential_index_stream &&
|
|
contiguous_decode && contiguous_first == first_index_value && decode_count == count;
|
|
if (direct_nonindexed_gpu_draw_enabled() &&
|
|
(primitive == 3u || primitive == 4u) &&
|
|
(primitive != 3u || (count % 3u) == 0u) && decode_count == count &&
|
|
(!indexed || identity_indexed_triangle_list)) {
|
|
needs_indices = false;
|
|
}
|
|
if (needs_indices) {
|
|
const std::size_t triangle_count = primitive == 3u ? count / 3u
|
|
: (count > 2u ? count - 2u : 0u);
|
|
triangle_indices.reserve(triangle_count * 3u);
|
|
if (primitive == 3u) {
|
|
for (std::size_t i = 0u; i + 2u < count; i += 3u) {
|
|
triangle_indices.push_back(occurrence_index(i));
|
|
triangle_indices.push_back(occurrence_index(i + 1u));
|
|
triangle_indices.push_back(occurrence_index(i + 2u));
|
|
}
|
|
} else if (primitive == 4u) {
|
|
// D3D12 natively supports triangle strips. Preserve the PSP
|
|
// strip index stream instead of manufacturing 3*(N-2)
|
|
// triangle-list indices on the CPU. IA handles alternating
|
|
// winding as part of strip assembly.
|
|
triangle_indices.reserve(count);
|
|
for (std::size_t i = 0u; i < count; ++i)
|
|
triangle_indices.push_back(occurrence_index(i));
|
|
} else {
|
|
for (std::size_t i = 1u; i + 1u < count; ++i) {
|
|
triangle_indices.push_back(occurrence_index(0u));
|
|
triangle_indices.push_back(occurrence_index(i));
|
|
triangle_indices.push_back(occurrence_index(i + 1u));
|
|
}
|
|
}
|
|
}
|
|
const std::size_t packed_triangle_count = primitive == 4u
|
|
? (count > 2u ? count - 2u : 0u)
|
|
: (needs_indices ? triangle_indices.size() / 3u : count / 3u);
|
|
bool accepted = false;
|
|
{
|
|
PhaseTimer accumulate_timer(g_ge_gpu_accumulate_ns);
|
|
accepted = ge_gpu_backend_accumulate_hardware_packed_0115(
|
|
effective_draw, hw,
|
|
std::span<const std::byte>(
|
|
reinterpret_cast<const std::byte *>(fast_0115_raw), fast_0115_bytes),
|
|
static_cast<std::uint32_t>(decode_count), triangle_indices);
|
|
}
|
|
if (accepted) {
|
|
if (collect_diagnostic_stats) stats.decoded_vertices += decode_count;
|
|
if (collect_diagnostic_stats) stats.triangles += packed_triangle_count;
|
|
advance_stream();
|
|
return true;
|
|
}
|
|
triangle_indices.clear();
|
|
}
|
|
|
|
// Fallback for lit/skinned/morphed/sparse/debug draws and for any backend
|
|
// that does not implement the packed 0x0115 stream.
|
|
decoded_vertices.resize(decode_count);
|
|
{
|
|
PhaseTimer vertex_timer(g_ge_vertex_decode_ns);
|
|
const auto decode_one = [&](std::size_t i, std::string &decode_error) -> bool {
|
|
const std::uint32_t index = contiguous_decode
|
|
? contiguous_first + static_cast<std::uint32_t>(i) : unique_indices[i];
|
|
if (fast_0115_raw != nullptr) {
|
|
return decode_model_vertex_0115_for_gpu_fast(
|
|
memory, vertex_address + index * layout.stride, layout, transform, commands,
|
|
cpu_lighting_effective, cpu_lighting_effective ? &prepared_lighting : nullptr,
|
|
decoded_vertices[i], decode_error,
|
|
fast_0115_raw + i * static_cast<std::size_t>(layout.stride),
|
|
fast_0115_world_normal_ptr);
|
|
}
|
|
return decode_model_vertex_for_gpu(memory, vertex_address + index * layout.stride,
|
|
layout, transform, commands,
|
|
cpu_lighting_effective,
|
|
cpu_lighting_effective ? &prepared_lighting : nullptr,
|
|
uv_generation, decoded_vertices[i], decode_error);
|
|
};
|
|
|
|
RowWorkerPool &decode_pool = RowWorkerPool::instance();
|
|
const bool expensive_vertex = cpu_lighting_effective || layout.weight_type != 0u;
|
|
const bool parallel_decode = parallel_vertex_decode_enabled() &&
|
|
decode_pool.worker_count() > 1u && decode_count >=
|
|
parallel_vertex_decode_threshold(expensive_vertex) &&
|
|
decode_count <= static_cast<std::size_t>(std::numeric_limits<std::int32_t>::max());
|
|
if (parallel_decode) {
|
|
std::atomic<bool> decode_failed{false};
|
|
std::array<std::string, RowWorkerPool::kMaxThreads> decode_errors{};
|
|
const std::size_t vertices_per_participant = expensive_vertex ? 64u : 128u;
|
|
const unsigned useful_participants = std::max(2u, std::min<unsigned>(
|
|
std::min(decode_pool.worker_count(), parallel_vertex_decode_max_participants()),
|
|
static_cast<unsigned>((decode_count + vertices_per_participant - 1u) /
|
|
vertices_per_participant)));
|
|
decode_pool.run(0, static_cast<std::int32_t>(decode_count - 1u),
|
|
[&](unsigned participant, std::int32_t first, std::int32_t last) {
|
|
std::string &local_error = decode_errors[participant];
|
|
for (std::int32_t row = first; row <= last; ++row) {
|
|
if (decode_failed.load(std::memory_order_relaxed)) break;
|
|
if (!decode_one(static_cast<std::size_t>(row), local_error)) {
|
|
decode_failed.store(true, std::memory_order_relaxed);
|
|
break;
|
|
}
|
|
}
|
|
}, useful_participants);
|
|
if (decode_failed.load(std::memory_order_relaxed)) {
|
|
for (const std::string &local_error : decode_errors) {
|
|
if (!local_error.empty()) { error = local_error; break; }
|
|
}
|
|
if (error.empty()) error = "parallel hardware vertex decode failed";
|
|
return false;
|
|
}
|
|
} else {
|
|
for (std::size_t i = 0u; i < decode_count; ++i) {
|
|
if (!decode_one(i, error)) return false;
|
|
}
|
|
}
|
|
if (collect_diagnostic_stats) stats.decoded_vertices += decoded_vertices.size();
|
|
}
|
|
|
|
const std::uint32_t alpha_control = pack_gpu_alpha_control(effective_draw);
|
|
const std::uint32_t fog_control = pack_gpu_fog_control(effective_draw);
|
|
std::uint32_t transform_control = 1u;
|
|
if (hw.cull_enabled) transform_control |= 2u;
|
|
if (hw.accept_counter_clockwise) transform_control |= 4u;
|
|
if (hw.depth_clip_enabled) transform_control |= 8u;
|
|
|
|
auto packed_to_color = [](std::uint32_t rgba) noexcept {
|
|
return Color{static_cast<std::uint8_t>(rgba & 0xFFu),
|
|
static_cast<std::uint8_t>((rgba >> 8u) & 0xFFu),
|
|
static_cast<std::uint8_t>((rgba >> 16u) & 0xFFu),
|
|
static_cast<std::uint8_t>((rgba >> 24u) & 0xFFu)};
|
|
};
|
|
auto finalize_vertex = [&](GeGpuVertex vertex) {
|
|
Color color = packed_to_color(vertex.rgba);
|
|
if (gpu_force_white_vertex_colors_enabled()) {
|
|
color = Color{255u, 255u, 255u, 255u};
|
|
} else if (gpu_geometry_debug_colors_enabled()) {
|
|
color = gpu_draw_debug_color(effective_draw);
|
|
} else if (effective_draw.texture_enabled && !sampled_texture_ready) {
|
|
color = gpu_texture_debug_color(effective_draw, color);
|
|
}
|
|
vertex.rgba = pack_gpu_color(color);
|
|
vertex.alpha_control = alpha_control;
|
|
vertex.texture_control = 0u; // backend fills current texture function.
|
|
vertex.texture_env = effective_draw.texture_env;
|
|
vertex.fog_control = fog_control;
|
|
vertex.transform_control = transform_control;
|
|
return vertex;
|
|
};
|
|
|
|
// Stage 43: smooth shading no longer copies the entire decoded vertex
|
|
// vector into a second submission vector. The decoded storage is private
|
|
// to this draw, so finalize it in place. Flat shading still needs
|
|
// triangle-local duplication because provoking-vertex colour semantics
|
|
// intentionally assign one colour to all three compact model vertices.
|
|
if (!flat_shading) {
|
|
for (GeGpuVertex &vertex : decoded_vertices)
|
|
vertex = finalize_vertex(vertex);
|
|
|
|
// The most common model path -- non-indexed PSP TRIANGLES -- is
|
|
// already an exact triangle stream. Do not manufacture and upload
|
|
// 0,1,2,3,... uint32 indices merely to call vkCmdDrawIndexed. An
|
|
// empty index span tells the backend to merge/submit it with vkCmdDraw.
|
|
if (direct_nonindexed_gpu_draw_enabled() &&
|
|
!indexed && primitive == 3u && (count % 3u) == 0u &&
|
|
decoded_vertices.size() == count) {
|
|
if (collect_diagnostic_stats) stats.triangles += count / 3u;
|
|
{
|
|
PhaseTimer accumulate_timer(g_ge_gpu_accumulate_ns);
|
|
ge_gpu_backend_accumulate_hardware_triangles(
|
|
effective_draw, hw, decoded_vertices, {});
|
|
}
|
|
advance_stream();
|
|
return true;
|
|
}
|
|
}
|
|
const auto occurrence_index = [&](std::size_t i) -> std::uint32_t {
|
|
if (!indexed) return static_cast<std::uint32_t>(i);
|
|
if (contiguous_decode) return occurrence_indices[i] - contiguous_first;
|
|
return occurrence_remap[i];
|
|
};
|
|
auto emit_triangle = [&](std::uint32_t ia, std::uint32_t ib, std::uint32_t ic) {
|
|
if (flat_shading) {
|
|
GeGpuVertex a = decoded_vertices[ia];
|
|
GeGpuVertex b = decoded_vertices[ib];
|
|
GeGpuVertex c = decoded_vertices[ic];
|
|
const std::uint32_t flat_color = c.rgba;
|
|
a.rgba = b.rgba = c.rgba = flat_color;
|
|
const std::uint32_t base = static_cast<std::uint32_t>(submitted_vertices.size());
|
|
submitted_vertices.push_back(finalize_vertex(a));
|
|
submitted_vertices.push_back(finalize_vertex(b));
|
|
submitted_vertices.push_back(finalize_vertex(c));
|
|
triangle_indices.push_back(base + 0u);
|
|
triangle_indices.push_back(base + 1u);
|
|
triangle_indices.push_back(base + 2u);
|
|
++stats.flat_shaded_primitives;
|
|
} else {
|
|
triangle_indices.push_back(ia);
|
|
triangle_indices.push_back(ib);
|
|
triangle_indices.push_back(ic);
|
|
}
|
|
if (collect_diagnostic_stats) ++stats.triangles;
|
|
};
|
|
|
|
const std::size_t triangle_count = primitive == 3u ? count / 3u
|
|
: (count > 2u ? count - 2u : 0u);
|
|
triangle_indices.reserve(triangle_count * 3u);
|
|
if (flat_shading) submitted_vertices.reserve(triangle_count * 3u);
|
|
if (primitive == 3u) {
|
|
for (std::size_t i = 0u; i + 2u < count; i += 3u)
|
|
emit_triangle(occurrence_index(i), occurrence_index(i + 1u),
|
|
occurrence_index(i + 2u));
|
|
} else if (primitive == 4u) {
|
|
for (std::size_t i = 0u; i + 2u < count; ++i) {
|
|
if ((i & 1u) == 0u)
|
|
emit_triangle(occurrence_index(i), occurrence_index(i + 1u),
|
|
occurrence_index(i + 2u));
|
|
else
|
|
emit_triangle(occurrence_index(i + 1u), occurrence_index(i),
|
|
occurrence_index(i + 2u));
|
|
}
|
|
} else {
|
|
for (std::size_t i = 1u; i + 1u < count; ++i)
|
|
emit_triangle(occurrence_index(0u), occurrence_index(i),
|
|
occurrence_index(i + 1u));
|
|
}
|
|
|
|
{
|
|
PhaseTimer accumulate_timer(g_ge_gpu_accumulate_ns);
|
|
const std::span<const GeGpuVertex> upload_vertices = flat_shading
|
|
? std::span<const GeGpuVertex>(submitted_vertices)
|
|
: std::span<const GeGpuVertex>(decoded_vertices);
|
|
// This fallback explicitly materializes triangles above (including
|
|
// strips/fans), so submit it as a list. Packed/raw fast paths return
|
|
// earlier and preserve native strip topology.
|
|
GeGpuHardwareTransform fallback_hw = hw;
|
|
fallback_hw.primitive = 3u;
|
|
ge_gpu_backend_accumulate_hardware_triangles(
|
|
effective_draw, fallback_hw, upload_vertices, triangle_indices);
|
|
}
|
|
advance_stream();
|
|
return true;
|
|
}
|
|
|
|
// Legacy/through/software paths really do consume FragmentSetup. Resolve
|
|
// and bind it only after the hardware-transform early return above.
|
|
FragmentSetup setup = make_fragment_setup_cached(commands);
|
|
{
|
|
PhaseTimer bind_timer(g_ge_draw_setup_ns);
|
|
bind_fragment_buffers(setup, memory, commands);
|
|
}
|
|
|
|
// Reused across draw calls. A heavy VCS vblank issues about 1200 of them,
|
|
// and each one used to allocate and free this vector plus the prepared
|
|
// triangle list below.
|
|
static thread_local std::vector<Vertex> vertices;
|
|
{
|
|
PhaseTimer vertex_timer(g_ge_vertex_decode_ns);
|
|
vertices.clear();
|
|
vertices.reserve(count);
|
|
const IndexStreamReader draw_indices =
|
|
make_index_reader(memory, index_address, layout.index_type, count);
|
|
for (std::uint32_t i = 0u; i < count; ++i) {
|
|
const std::uint32_t index = draw_indices(i);
|
|
Vertex vertex{};
|
|
if (!decode_vertex_optimized(memory, vertex_address + index * layout.stride, layout,
|
|
commands, transform, vertex, error)) return false;
|
|
record_clip_vertex(stats, vertex);
|
|
if (layout.through) record_screen_vertex(stats, vertex);
|
|
vertices.push_back(vertex);
|
|
}
|
|
}
|
|
|
|
// Widescreen: shrink the 2D interface back into the game's 16:9 box.
|
|
//
|
|
// Ported from ThirteenAG's WidescreenFixesPack, plugin
|
|
// GTAVCS.WidescreenFix -- MIT License, Copyright (c) 2018 ThirteenAG.
|
|
// His CRect::CRect hook does the same arithmetic on the game's own HUD
|
|
// rectangles, full-width ones exempted so backdrops keep covering.
|
|
//
|
|
// Here rather than in the backend because both renderers read this vector.
|
|
// The software rasterizer keeps filling guest VRAM for targets the GPU does
|
|
// not own, and VCS spends every other vblank replaying that VRAM through
|
|
// composition quads -- so a correction applied only to the Vulkan vertices
|
|
// produced two HUDs, corrected and stretched, alternating at 30 Hz.
|
|
if (layout.through && gpu_backend_enabled && !vertices.empty()) {
|
|
float min_x = vertices.front().x;
|
|
float max_x = vertices.front().x;
|
|
float max_y = vertices.front().y;
|
|
for (const Vertex &vertex : vertices) {
|
|
min_x = std::min(min_x, vertex.x);
|
|
max_x = std::max(max_x, vertex.x);
|
|
max_y = std::max(max_y, vertex.y);
|
|
}
|
|
ge_gpu_backend_note_through_extent(gpu_draw, max_x, max_y);
|
|
|
|
const GeGpuWidescreenHud hud = ge_gpu_backend_widescreen_hud(gpu_draw);
|
|
// Full-width draws are backdrops, fades and letterbox bars: they have to
|
|
// keep covering the screen, so they stay stretched. Measured against the
|
|
// real 480 px display, not against this target's own size.
|
|
constexpr float kFullWidthFraction = 0.9f;
|
|
const bool full_width = (max_x - min_x) * hud.display_scale_x >=
|
|
kFullWidthFraction * 480.0f;
|
|
// The ThirteenAG rectangle correction is a HUD/surface transform, not a
|
|
// generic transform for every through-mode primitive. Stage 45.3 also
|
|
// moved POINTS/LINES/LINE_STRIP (0..2); VCS uses tiny helper primitives
|
|
// around the radar and that exposed the grey line + magenta point seen
|
|
// above the minimap. Keep those auxiliary primitives in PSP coordinates
|
|
// and only correct filled geometry (triangles/strips/fans/sprites).
|
|
const bool widescreen_surface_primitive = primitive >= 3u && primitive <= 6u;
|
|
if (widescreen_surface_primitive && hud.shrink != 1.0f && !setup.clear_mode && !full_width &&
|
|
// Additive depth-tested 2D is not interface, it is the world drawn
|
|
// in screen space: coronas, headlight glows, lens flares. The guest
|
|
// already projected those from world positions, so they are correct
|
|
// where they are. Shrinking them toward the middle of the screen
|
|
// displaces each one in proportion to its distance from the centre,
|
|
// and the shrunk clip rectangle then cuts the ones near the edges.
|
|
//
|
|
// Both conditions are needed. The depth test alone also describes
|
|
// the radar, which is depth-tested and *is* interface -- exempting
|
|
// it left the map unshrunk inside a shrunk frame. What separates the
|
|
// two is the blend: a glow adds light with a fixed destination
|
|
// factor, while the radar composites with ordinary source alpha.
|
|
!(setup.depth_test_enabled && !setup.depth_write_enabled) &&
|
|
// The composition quads are through-mode too, and they carry the
|
|
// world. Shrinking them would pillarbox the picture and undo the
|
|
// widened frustum instead of complementing it.
|
|
!(setup.texture_enabled && ge_gpu_backend_is_framebuffer_feedback_texture(gpu_draw))) {
|
|
for (Vertex &vertex : vertices)
|
|
vertex.x = hud.source_center + (vertex.x - hud.source_center) / hud.shrink;
|
|
// The clip rectangle has to move with the geometry, or the radar
|
|
// keeps being masked where the radar used to be. Both renderers read
|
|
// their scissor from here.
|
|
// Rounded inwards, both ends: the rectangle is integral and the
|
|
// shrunk geometry is not, so rounding to nearest can leave the clip
|
|
// up to half a pixel wider than the quad it is meant to contain.
|
|
// That slack showed as a one-pixel column of the radar map leaking
|
|
// out either side of the radar's circular frame. Losing a pixel of
|
|
// an edge that a mask covers anyway is the harmless direction.
|
|
const auto shrink_scissor = [&](std::int32_t value, bool leading) {
|
|
const float moved = hud.source_center +
|
|
(static_cast<float>(value) - hud.source_center) / hud.shrink;
|
|
return static_cast<std::int32_t>(leading ? std::ceil(moved)
|
|
: std::floor(moved));
|
|
};
|
|
setup.scissor_x0 = shrink_scissor(setup.scissor_x0, true);
|
|
setup.scissor_x1 = shrink_scissor(setup.scissor_x1, false);
|
|
gpu_draw.scissor_x0 = setup.scissor_x0;
|
|
gpu_draw.scissor_x1 = setup.scissor_x1;
|
|
gpu_draw.widescreen_hud = true;
|
|
}
|
|
}
|
|
|
|
// Stage 22 relic: every triangle-stream vertex was converted and copied into
|
|
// the mapped Vulkan upload ring, and nothing ever read those bytes. The
|
|
// frame Vulkan draws is assembled from the prepared screen triangles below,
|
|
// and finish_color_frame overwrites the very same region with them. It cost
|
|
// one allocation and one full vertex conversion per draw call plus about
|
|
// 2 MB per frame of write-combined memcpy, all discarded.
|
|
if (legacy_vertex_staging_enabled() && gpu_backend_enabled &&
|
|
ge_gpu_backend_transfer_ready() &&
|
|
primitive >= 3u && primitive <= 5u && !gpu_draw.clear_mode) {
|
|
PhaseTimer stage_timer(g_ge_gpu_stage_ns);
|
|
std::vector<GeGpuVertex> gpu_vertices;
|
|
gpu_vertices.reserve(vertices.size());
|
|
for (const Vertex &vertex : vertices) {
|
|
const std::uint32_t rgba =
|
|
static_cast<std::uint32_t>(vertex.color.r) |
|
|
(static_cast<std::uint32_t>(vertex.color.g) << 8u) |
|
|
(static_cast<std::uint32_t>(vertex.color.b) << 16u) |
|
|
(static_cast<std::uint32_t>(vertex.color.a) << 24u);
|
|
gpu_vertices.push_back(GeGpuVertex{
|
|
vertex.x, vertex.y, vertex.z, vertex.w, rgba, vertex.u, vertex.v,
|
|
pack_gpu_alpha_control(gpu_draw), 0u, gpu_draw.texture_env,
|
|
vertex.fog_factor, pack_gpu_fog_control(gpu_draw), vertex.q,
|
|
});
|
|
}
|
|
(void)ge_gpu_backend_stage_vertices(gpu_draw, gpu_vertices);
|
|
}
|
|
|
|
switch (primitive) {
|
|
case 0u:
|
|
for (const Vertex &vertex : vertices) {
|
|
rasterize_point(memory, commands, setup, vertex, layout.through, stats);
|
|
if (collect_diagnostic_stats) ++stats.points;
|
|
}
|
|
break;
|
|
case 1u:
|
|
for (std::size_t i = 0u; i + 1u < vertices.size(); i += 2u) {
|
|
rasterize_line(memory, commands, setup, vertices[i], vertices[i + 1u], layout.through, stats);
|
|
if (collect_diagnostic_stats) ++stats.lines;
|
|
}
|
|
break;
|
|
case 2u:
|
|
for (std::size_t i = 0u; i + 1u < vertices.size(); ++i) {
|
|
rasterize_line(memory, commands, setup, vertices[i], vertices[i + 1u], layout.through, stats);
|
|
if (collect_diagnostic_stats) ++stats.lines;
|
|
}
|
|
break;
|
|
case 3u: {
|
|
static thread_local std::vector<PreparedScreenTriangle> triangles; triangles.clear();
|
|
{
|
|
PhaseTimer prep_timer(g_ge_triangle_prep_ns);
|
|
triangles.reserve(vertices.size() / 3u + 2u);
|
|
for (std::size_t i = 0u; i + 2u < vertices.size(); i += 3u) {
|
|
append_prepared_triangles(commands, setup,
|
|
vertices[i], vertices[i + 1u], vertices[i + 2u],
|
|
vertices[i + 2u], layout.through, stats, triangles, gpu_only_triangle_path);
|
|
if (collect_diagnostic_stats) ++stats.triangles;
|
|
}
|
|
}
|
|
if (gpu_backend_enabled) {
|
|
PhaseTimer accumulate_timer(g_ge_gpu_accumulate_ns);
|
|
accumulate_gpu_prepared_triangles(gpu_draw, triangles);
|
|
}
|
|
rasterize_prepared_triangles(memory, commands, setup, triangles, stats);
|
|
break;
|
|
}
|
|
case 4u: {
|
|
static thread_local std::vector<PreparedScreenTriangle> triangles; triangles.clear();
|
|
{
|
|
PhaseTimer prep_timer(g_ge_triangle_prep_ns);
|
|
triangles.reserve(vertices.size() + 2u);
|
|
for (std::size_t i = 0u; i + 2u < vertices.size(); ++i) {
|
|
if ((i & 1u) == 0u)
|
|
append_prepared_triangles(commands, setup,
|
|
vertices[i], vertices[i + 1u], vertices[i + 2u],
|
|
vertices[i + 2u], layout.through, stats, triangles, gpu_only_triangle_path);
|
|
else
|
|
append_prepared_triangles(commands, setup,
|
|
vertices[i + 1u], vertices[i], vertices[i + 2u],
|
|
vertices[i + 2u], layout.through, stats, triangles, gpu_only_triangle_path);
|
|
if (collect_diagnostic_stats) ++stats.triangles;
|
|
}
|
|
}
|
|
if (gpu_backend_enabled) {
|
|
PhaseTimer accumulate_timer(g_ge_gpu_accumulate_ns);
|
|
accumulate_gpu_prepared_triangles(gpu_draw, triangles);
|
|
}
|
|
rasterize_prepared_triangles(memory, commands, setup, triangles, stats);
|
|
break;
|
|
}
|
|
case 5u: {
|
|
static thread_local std::vector<PreparedScreenTriangle> triangles; triangles.clear();
|
|
{
|
|
PhaseTimer prep_timer(g_ge_triangle_prep_ns);
|
|
triangles.reserve(vertices.size() + 2u);
|
|
for (std::size_t i = 1u; i + 1u < vertices.size(); ++i) {
|
|
append_prepared_triangles(commands, setup,
|
|
vertices[0], vertices[i], vertices[i + 1u],
|
|
vertices[i + 1u], layout.through, stats, triangles, gpu_only_triangle_path);
|
|
if (collect_diagnostic_stats) ++stats.triangles;
|
|
}
|
|
}
|
|
if (gpu_backend_enabled) {
|
|
PhaseTimer accumulate_timer(g_ge_gpu_accumulate_ns);
|
|
accumulate_gpu_prepared_triangles(gpu_draw, triangles);
|
|
}
|
|
rasterize_prepared_triangles(memory, commands, setup, triangles, stats);
|
|
break;
|
|
}
|
|
case 6u:
|
|
for (std::size_t i = 0u; i + 1u < vertices.size(); i += 2u) {
|
|
if (gpu_backend_enabled)
|
|
accumulate_gpu_rectangle(gpu_draw, vertices[i], vertices[i + 1u]);
|
|
rasterize_rectangle(memory, commands, setup, vertices[i], vertices[i + 1u], stats);
|
|
if (collect_diagnostic_stats) ++stats.rectangles;
|
|
}
|
|
break;
|
|
default:
|
|
if (collect_diagnostic_stats) ++stats.unsupported_primitives;
|
|
break;
|
|
}
|
|
advance_stream();
|
|
return true;
|
|
}
|
|
|
|
GePhaseTotals ge_phase_totals() noexcept {
|
|
return GePhaseTotals{
|
|
g_ge_pixel_ns, g_ge_triangle_count,
|
|
g_ge_draw_setup_ns, g_ge_texture_upload_ns, g_ge_vertex_decode_ns,
|
|
g_ge_gpu_stage_ns, g_ge_triangle_prep_ns, g_ge_gpu_accumulate_ns,
|
|
g_ge_primitive_count, g_ge_vertex_count,
|
|
};
|
|
}
|
|
|
|
void reset_ge_phase_totals() noexcept {
|
|
g_ge_pixel_ns = 0u;
|
|
g_ge_triangle_count = 0u;
|
|
g_ge_draw_setup_ns = 0u;
|
|
g_ge_texture_upload_ns = 0u;
|
|
g_ge_vertex_decode_ns = 0u;
|
|
g_ge_gpu_stage_ns = 0u;
|
|
g_ge_triangle_prep_ns = 0u;
|
|
g_ge_gpu_accumulate_ns = 0u;
|
|
g_ge_primitive_count = 0u;
|
|
g_ge_vertex_count = 0u;
|
|
}
|
|
|
|
} // namespace vcs
|