#include "ge_renderer.hpp" #include "ge_gpu_backend.hpp" #include "lcs_controls.hpp" #include "lcs_fps_overlay.hpp" #include "lcs_render_config.hpp" #include "psprecomp/common.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(_M_X64) || defined(_M_IX86) || defined(__x86_64__) || defined(__i386__) #include #define PSPRECOMP_GE_X86_SIMD 1 #else #define PSPRECOMP_GE_X86_SIMD 0 #endif namespace lcs { namespace { [[nodiscard]] inline bool finite_float(float value) noexcept { return (std::bit_cast(value) & 0x7F800000u) != 0x7F800000u; } [[nodiscard]] inline std::int32_t floor_to_int(float value) noexcept { if (!(value > -2147483000.0f && value < 2147483000.0f)) return static_cast(std::floor(value)); const std::int32_t truncated = static_cast(value); return truncated - static_cast(static_cast(truncated) > value); } [[nodiscard]] inline float floor_float(float value) noexcept { if (!(value > -2147483000.0f && value < 2147483000.0f)) return std::floor(value); const float truncated = static_cast(static_cast(value)); return truncated - static_cast(truncated > value); } [[nodiscard]] inline std::uint8_t round_clamp_to_byte(float value) noexcept { if (value > -0.5f && value < 255.5f) { const long truncated = static_cast(value); const float fraction = value - static_cast(truncated); const long rounded = truncated + static_cast(fraction >= 0.5f) - static_cast(fraction <= -0.5f); return static_cast(rounded < 0L ? 0L : (rounded > 255L ? 255L : rounded)); } return static_cast(std::clamp(std::lround(value), 0L, 255L)); } 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 } 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{}; 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{}; 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::chrono::duration_cast( std::chrono::steady_clock::now() - entry).count()); } PhaseTimer(const PhaseTimer &) = delete; PhaseTimer &operator=(const PhaseTimer &) = delete; }; 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; } float lcs_hud_scale() noexcept { static const float scale = [] { const LcsConfiguration &config = lcs_render_configuration(); return config.initialized ? std::clamp(config.display.hud_scale, 0.25f, 1.0f) : 1.0f; }(); return scale; } bool gpu_hardware_transform_enabled() noexcept { 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 lcs::LcsConfiguration &config = lcs::lcs_render_configuration(); return config.initialized && config.rendering.hardware_transform; }(); return enabled; } 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; } 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(value); }(); return threshold; } bool parallel_vertex_decode_enabled() noexcept { 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 { 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 { 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(value); }(); return expensive_vertex ? std::max(64u, simple_threshold / 2u) : simple_threshold; } unsigned parallel_vertex_decode_max_participants() noexcept { 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(value); }(); return participants; } inline void raster_cpu_relax() noexcept { #if PSPRECOMP_GE_X86_SIMD _mm_pause(); #else 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::chrono::duration_cast( std::chrono::steady_clock::now() - entry).count()); } }; 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_; } template 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( std::min(worker_count_, participant_limit), static_cast(rows)); if (participants <= 1u || workers_.empty()) { body(0u, first, last); return; } const unsigned chunks = std::min( static_cast(rows), participants * kChunksPerParticipant); using BodyType = std::remove_reference_t; body_context_ = static_cast(&body); body_invoke_ = [](void *context, unsigned participant, std::int32_t begin, std::int32_t end) { (*static_cast(context))(participant, begin, end); }; range_first_ = first; rows_per_chunk_ = rows / static_cast(chunks); remainder_ = rows % static_cast(chunks); chunk_count_ = chunks; active_participants_ = participants; next_chunk_.store(0u, std::memory_order_relaxed); pending_.store(participants - 1u, std::memory_order_relaxed); generation_.fetch_add(1u, std::memory_order_release); if (parked_.load(std::memory_order_acquire) != 0u) generation_.notify_all(); run_available_chunks(0u); 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(value); explicitly_configured = true; } } if (!explicitly_configured) { constexpr unsigned kDefaultRowWorkers = 8u; requested = std::min({requested, kMaxThreads, kDefaultRowWorkers}); } 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(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) { 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); 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; static constexpr unsigned kWorkerSpins = 262144u; std::vector workers_; void *body_context_{}; BodyInvoke body_invoke_{}; std::atomic next_chunk_{}; std::atomic pending_{}; std::atomic parked_{}; unsigned chunk_count_{}; unsigned active_participants_{}; std::int32_t range_first_{}; std::int32_t rows_per_chunk_{}; std::int32_t remainder_{}; std::atomic generation_{}; std::atomic stopping_{}; unsigned worker_count_{1u}; }; float decode_float24(std::uint32_t data) noexcept { return std::bit_cast((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: { 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}; 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 tc_size{0u, 2u, 4u, 8u}; static constexpr std::array tc_align{1u, 1u, 2u, 4u}; static constexpr std::array color_size{0u, 0u, 0u, 0u, 2u, 2u, 2u, 4u}; static constexpr std::array color_align{1u, 1u, 1u, 1u, 2u, 2u, 2u, 4u}; static constexpr std::array normal_size{0u, 3u, 6u, 12u}; static constexpr std::array normal_align{1u, 1u, 2u, 4u}; static constexpr std::array position_size{0u, 3u, 6u, 12u}; static constexpr std::array position_align{1u, 1u, 2u, 4u}; static constexpr std::array weight_size{0u, 1u, 2u, 4u}; static constexpr std::array 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((value << 4u) | value); } std::uint8_t expand5(std::uint32_t value) noexcept { value &= 0x1Fu; return static_cast((value << 3u) | (value >> 2u)); } std::uint8_t expand6(std::uint32_t value) noexcept { value &= 0x3Fu; return static_cast((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((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(pixel), static_cast(pixel >> 8u), static_cast(pixel >> 16u), static_cast(pixel >> 24u)}; } std::uint32_t pack32(Color color) noexcept { return static_cast(color.r) | (static_cast(color.g) << 8u) | (static_cast(color.b) << 16u) | (static_cast(color.a) << 24u); } std::uint16_t pack16(Color color, std::uint32_t format) noexcept { switch (format) { case 0u: return static_cast((color.r >> 3u) | ((color.g >> 2u) << 5u) | ((color.b >> 3u) << 11u)); case 1u: return static_cast((color.r >> 3u) | ((color.g >> 3u) << 5u) | ((color.b >> 3u) << 10u) | ((color.a >= 128u ? 1u : 0u) << 15u)); case 2u: return static_cast((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); } Color read_color_raw(const std::uint8_t *pixel, std::uint32_t format) noexcept { if (format == 3u) { return unpack32(static_cast(pixel[0]) | (static_cast(pixel[1]) << 8u) | (static_cast(pixel[2]) << 16u) | (static_cast(pixel[3]) << 24u)); } return unpack16(static_cast( static_cast(pixel[0]) | (static_cast(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(pixel[0]) | (static_cast(pixel[1]) << 8u) | (static_cast(pixel[2]) << 16u) | (static_cast(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(value); pixel[1] = static_cast(value >> 8u); pixel[2] = static_cast(value >> 16u); pixel[3] = static_cast(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(value); pixel[1] = static_cast(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 { 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(static_cast(value)) / 128.0f; } float signed_normalized16(std::uint16_t value) noexcept { return static_cast(static_cast(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 &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 &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 &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 affine_4x3_to_mat4(const std::array &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 multiply_mat4(const std::array &a, const std::array &b) noexcept { std::array 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 &commands, const GeTransformState &transform, 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{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); 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(data24(commands[0x4Cu]) & 0xFFFFu) / 16.0f; hw.viewport_offset_y = static_cast(data24(commands[0x4Du]) & 0xFFFFu) / 16.0f; 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(memory.aot_load32(address)), std::bit_cast(memory.aot_load32(address + 4u)), std::bit_cast(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(memory.aot_load8(address)); v = static_cast(memory.aot_load8(address + 1u)); if (!through) { u *= 1.0f / 128.0f; v *= 1.0f / 128.0f; } break; case 2u: u = static_cast(memory.aot_load16(address)); v = static_cast(memory.aot_load16(address + 2u)); if (!through) { u *= 1.0f / 32768.0f; v *= 1.0f / 32768.0f; } break; case 3u: u = std::bit_cast(memory.aot_load32(address)); v = std::bit_cast(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::clamp(static_cast(value), 0l, 255l)); } Color material_ambient_color(const std::array &commands) noexcept { const std::uint32_t rgb = data24(commands[0x55u]); return Color{ static_cast(rgb & 0xFFu), static_cast((rgb >> 8u) & 0xFFu), static_cast((rgb >> 16u) & 0xFFu), static_cast(data24(commands[0x58u]) & 0xFFu), }; } Color morph_color(const psprecomp::GuestMemory &memory, std::uint32_t address, const VertexLayout &layout, const GeTransformState &transform, const std::array &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 compute_skin_matrix(const psprecomp::GuestMemory &memory, std::uint32_t address, const VertexLayout &layout, const GeTransformState &transform) { std::array 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(memory.aot_load32(address + layout.weight_offset + bone * 4u)); break; } if (weight == 0.0f) continue; const std::size_t bone_base = static_cast(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::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 lights{}; }; PreparedLighting prepare_lighting(bool has_vertex_color, const std::array &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(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(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; 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)); } } } return from_float_color(result); } Color apply_lighting(Color input, bool has_vertex_color, Vec3 world_position, Vec3 world_normal, const std::array &commands) { return apply_prepared_lighting(input, world_position, world_normal, prepare_lighting(has_vertex_color, commands)); } bool prepare_directional_lighting_affine(const PreparedLighting &state, Vec3 world_normal, std::array &mul, std::array &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 &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(static_cast(memory.aot_load16(address + layout.position_offset))); vertex.y = static_cast(static_cast(memory.aot_load16(address + layout.position_offset + 2u))); vertex.z = static_cast(memory.aot_load16(address + layout.position_offset + 4u)); break; case 3u: vertex.x = std::bit_cast(memory.aot_load32(address + layout.position_offset)); vertex.y = std::bit_cast(memory.aot_load32(address + layout.position_offset + 4u)); vertex.z = std::clamp(std::bit_cast(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; } 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; const std::uint32_t uv_generation_source = (uv_generation_data >> 8u) & 3u; const bool lighting_enabled = (data24(commands[0x17u]) & 1u) != 0u; 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 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); 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(1u << (texture_size & 0xFu)); const float texture_height = static_cast(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 &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; } vertex.u = static_cast(memory.aot_load8(address + layout.tc_offset)) * (1.0f / 128.0f); vertex.v = static_cast(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); 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(1u << (texture_size & 0xFu)); const float texture_height = static_cast(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 &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 &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) { 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(raw[offset]) | (static_cast(raw[offset + 1u]) << 8u); }; const float u = static_cast(raw[layout.tc_offset]) * (1.0f / 128.0f); const float v = static_cast(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(final_color.r) | (static_cast(final_color.g) << 8u) | (static_cast(final_color.b) << 16u) | (static_cast(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 &commands, bool lighting_enabled, const PreparedLighting *prepared_lighting, std::uint32_t uv_generation, GeGpuVertex &vertex, std::string &error) { 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 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); } 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; } Color final_color = color; if (lighting_enabled) { 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(final_color.r) | (static_cast(final_color.g) << 8u) | (static_cast(final_color.b) << 16u) | (static_cast(final_color.a) << 24u); return true; } std::uint32_t framebuffer_address(const std::array &commands) noexcept { return kVramBase | (data24(commands[0x9Cu]) & 0x001FFFF0u); } std::uint32_t depthbuffer_address(const std::array &commands) noexcept { return kVramBase | (data24(commands[0x9Eu]) & 0x001FFFF0u); } std::uint32_t texture_address(const std::array &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 &commands) noexcept { return texture_address(commands, 0u); } bool software_raster_skipped(const std::array &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'; const LcsConfiguration &cfg = lcs_render_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(raw) - 256 : static_cast(raw); } std::uint32_t selected_texture_level(const std::array &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; if (mode != 1u) return 0u; const float lod = static_cast(signed_texture_lod_offset16(texlevel)) / 16.0f; const bool mip_linear = (texfilter & 2u) != 0u; const int selected = mip_linear ? static_cast(std::floor(lod)) : static_cast(std::floor(lod + 0.5f)); return static_cast(std::clamp(selected, 0, static_cast(max_level))); } std::uint32_t clut_address(const std::array &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::clamp(value, 0, static_cast(dimension - 1u))); if ((dimension & (dimension - 1u)) == 0u) return static_cast(value) & (dimension - 1u); const std::int32_t d = static_cast(dimension); std::int32_t result = value % d; if (result < 0) result += d; return static_cast(result); } Color read_clut(const psprecomp::GuestMemory &memory, const std::array &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); } 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 &commands, std::uint32_t requested_level) noexcept { TextureSetup setup{}; setup.selected_level=std::min(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( 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); const std::uint32_t row_bytes = setup.buffer_width * 4u; const std::uint64_t span = static_cast(setup.height + 8u) * (row_bytes + 128u); setup.pixels = memory.raw_pointer(setup.base, static_cast(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 &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(texture.buffer_width) * texture.height * 2u; break; case 3u: case 7u: bytes = static_cast(texture.buffer_width) * texture.height * 4u; break; case 4u: bytes = static_cast((texture.buffer_width + 1u) >> 1u) * texture.height; break; case 5u: bytes = static_cast(texture.buffer_width) * texture.height; break; case 8u: bytes = static_cast((texture.buffer_width + 3u) >> 2u) * ((texture.height + 3u) >> 2u) * 8u; break; case 9u: case 10u: bytes = static_cast((texture.buffer_width + 3u) >> 2u) * ((texture.height + 3u) >> 2u) * 16u; break; default: return 0u; } if (texture.swizzled && texture.format <= 7u) { std::uint64_t row = bytes / std::max(1u, texture.height); row = (row + 15u) & ~15ull; bytes = row * ((static_cast(texture.height) + 7u) & ~7ull); } if (bytes == 0u || bytes > std::numeric_limits::max()) return 0u; const auto size = static_cast(bytes); const std::uint8_t *pixels = memory.raw_pointer(texture.base, size); if (pixels == nullptr) return 0u; 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(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(size) + (static_cast(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(entry[0]) | (static_cast(entry[1]) << 8u) | (static_cast(entry[2]) << 16u) | (static_cast(entry[3]) << 24u)); } return unpack16(static_cast( static_cast(entry[0]) | (static_cast(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(static_cast(p[0]) | (static_cast(p[1]) << 8u)); } std::uint32_t load_le32(const std::uint8_t *p) noexcept { return static_cast(p[0]) | (static_cast(p[1]) << 8u) | (static_cast(p[2]) << 16u) | (static_cast(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 { return Color{ static_cast((c >> 8u) & 0xF8u), static_cast((c >> 3u) & 0xFCu), static_cast((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((static_cast(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((static_cast(a.r) + b.r) / 2u), static_cast((static_cast(a.g) + b.g) / 2u), static_cast((static_cast(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 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> (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(((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(alpha_data_high) << 32u) | alpha_data_low; const std::uint32_t alpha_index = static_cast( (alpha_bits >> (row * 12u + column * 3u)) & 7u); const std::uint8_t alpha1 = block[14u]; const std::uint8_t alpha2 = block[15u]; std::array 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(alpha1) * ((7u-i) << 8u)) / 7u + (static_cast(alpha2) * (i << 8u)) / 7u; palette[i+1u] = static_cast((fixed + 31u) >> 8u); } } else { for (std::uint32_t i=1u;i<=4u;++i) { const unsigned fixed = (static_cast(alpha1) * ((5u-i) << 8u)) / 5u + (static_cast(alpha2) * (i << 8u)) / 5u; palette[i+1u] = static_cast((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; 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(static_cast(pixels[offset]) | (static_cast(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(pixels[offset]) | (static_cast(pixels[offset + 1u]) << 8u) | (static_cast(pixels[offset + 2u]) << 16u) | (static_cast(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(a.a), static_cast(a.b), static_cast(a.g), static_cast(a.r)); const __m128 bv = _mm_set_ps(static_cast(b.a), static_cast(b.b), static_cast(b.g), static_cast(b.r)); const __m128 result = _mm_add_ps(av, _mm_mul_ps(_mm_sub_ps(bv, av), _mm_set1_ps(t))); 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(_mm_cvtsi128_si32(packed8)); return std::bit_cast(packed); #else auto channel = [t](std::uint8_t x, std::uint8_t y) { return round_clamp_to_byte(static_cast(x) + (static_cast(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(bx); const float fy = shifted_v - static_cast(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); 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((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 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(texture.width) * texture.height * 4ull; if (byte_count != rgba8.size()) return false; std::array t4_palette{}; std::array 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(c.r) | (static_cast(c.g) << 8u) | (static_cast(c.b) << 16u) | (static_cast(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(y) * texture.width * 4u; const auto source_offset = [&](std::uint32_t byte_x) noexcept { if (!texture.swizzled) return static_cast(y) * row_bytes + byte_x; return static_cast( (((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(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(color.r); rgba8[offset + 1u] = static_cast(color.g); rgba8[offset + 2u] = static_cast(color.b); rgba8[offset + 3u] = static_cast(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(); const std::uint64_t texels = static_cast(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 { const std::uint64_t texels_per_participant = fast_t4 ? 8192u : 4096u; const unsigned useful_participants = std::max(2u, std::min( pool.worker_count(), static_cast((texels + texels_per_participant - 1u) / texels_per_participant))); pool.run(0, static_cast(texture.height) - 1, [&decode_rows](unsigned, std::int32_t begin, std::int32_t end) { decode_rows(static_cast(begin), static_cast(end)); }, useful_participants); } return true; } bool decode_texture_rgba(const psprecomp::GuestMemory &memory, const TextureSetup &texture, std::vector &rgba8) { const std::uint64_t byte_count = static_cast(texture.width) * texture.height * 4ull; if (byte_count > static_cast(std::numeric_limits::max())) return false; try { rgba8.resize(static_cast(byte_count)); if (decode_texture_rgba_into(memory, texture, rgba8)) return true; } catch (...) { } rgba8.clear(); return false; } 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{}; std::uint8_t *color_pixels{}; std::uint8_t *depth_pixels{}; TextureSetup texture{}; 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{}; }; void bind_fragment_buffers(FragmentSetup &setup, psprecomp::GuestMemory &memory, const std::array &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(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 &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(scissor1 & 0x3FFu); setup.scissor_y0 = static_cast((scissor1 >> 10u) & 0x3FFu); setup.scissor_x1 = scissor2 == 0u ? static_cast(setup.framebuffer_stride - 1u) : static_cast(scissor2 & 0x3FFu); setup.scissor_y1 = scissor2 == 0u ? 271 : static_cast((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 &commands) noexcept { static constexpr std::array 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 values{}; FragmentSetup setup{}; bool valid{}; }; static thread_local Cache cache; std::array values{}; for (std::size_t i = 0; i + 1 < regs.size(); ++i) values[i] = commands[regs[i]]; 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((static_cast(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: 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: { const std::uint32_t alpha = use_alpha ? texture.a : 255u; auto decal = [alpha](std::uint8_t vc, std::uint8_t tc) { return static_cast((static_cast(tc) * alpha + static_cast(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: { const Color env = setup.texture_env; auto blend = [](std::uint8_t vc, std::uint8_t tc, std::uint8_t ec) { return static_cast((static_cast(vc) * (255u - tc) + static_cast(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: out = texture; if (!use_alpha) out.a = vertex.a; break; case 4u: out = {static_cast(std::min(255u, static_cast(vertex.r) + texture.r)), static_cast(std::min(255u, static_cast(vertex.g) + texture.g)), static_cast(std::min(255u, static_cast(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::min(255u, static_cast(out.r) * 2u)); out.g = static_cast(std::min(255u, static_cast(out.g) * 2u)); out.b = static_cast(std::min(255u, static_cast(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 color_float(Color c) noexcept { return {c.r / 255.0f, c.g / 255.0f, c.b / 255.0f, c.a / 255.0f}; } std::array blend_factor(std::uint32_t factor, bool source_factor, const std::array &src, const std::array &dst, Color fixed) noexcept { switch (factor) { case 0u: return source_factor ? std::array{dst[0], dst[1], dst[2], dst[3]} : std::array{src[0], src[1], src[2], src[3]}; case 1u: return source_factor ? std::array{1-dst[0],1-dst[1],1-dst[2],1-dst[3]} : std::array{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; 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((static_cast(src) * alpha + static_cast(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 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(y) * setup.depth_stride + static_cast(x); if (setup.depth_pixels != nullptr) { std::uint8_t *pixel = setup.depth_pixels + index * 2u; if (clear_depth) { pixel[0] = static_cast(z); pixel[1] = static_cast(z >> 8u); return true; } bool passed = true; if (setup.depth_test_enabled) { const std::uint16_t stored = static_cast( static_cast(pixel[0]) | (static_cast(pixel[1]) << 8u)); passed = compare_value(setup.depth_function, z, stored); } if (passed && setup.depth_write_enabled) { pixel[0] = static_cast(z); pixel[1] = static_cast(z >> 8u); } return passed; } const std::uint32_t address = setup.depth_base + static_cast(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 &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; 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::ceil(min_x_f)); std::int32_t x1 = static_cast(std::ceil(max_x_f)) - 1; std::int32_t y0 = static_cast(std::ceil(min_y_f)); std::int32_t y1 = static_cast(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(rectangle_width) * static_cast(y1 - y0 + 1); const bool phase_diag = ge_phase_diag_enabled(); RowWorkerPool &pool = RowWorkerPool::instance(); 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::clamp(static_cast(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(y) * framebuffer_stride + static_cast(x0)) * bpp; if (framebuffer_format == 3u) { std::fill_n(reinterpret_cast(row), static_cast(rectangle_width), packed32); } else { std::fill_n(reinterpret_cast(row), static_cast(rectangle_width), packed16); } } if (fast_depth_clear) { std::uint8_t *depth_row = setup.depth_pixels + (static_cast(y) * setup.depth_stride + static_cast(x0)) * 2u; std::fill_n(reinterpret_cast(depth_row), static_cast(rectangle_width), depth_value); } row_stats.pixels_tested += static_cast(rectangle_width); row_stats.pixels_written += static_cast(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 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; } 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(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(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(a.z) + (static_cast(b.z) - a.z) * ((tx + ty) * 0.5f); const std::uint16_t z = static_cast(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(y) * framebuffer_stride + static_cast(x); const std::uint32_t pixel_address = fb + static_cast(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 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; } struct ClipPolygon { std::array 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; 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; } 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 &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])); const float offset_x = static_cast(data24(commands[0x4Cu]) & 0xFFFFu) / 16.0f; const float offset_y = static_cast(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; } bool depth_precedes_shading(const FragmentSetup &setup) noexcept { return setup.clear_mode || !setup.alpha_test_enabled; } 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(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::clamp(zf, 0.0f, 65535.0f)); return depth_test_and_write(memory, setup, x, y, z, setup.clear_mode && setup.clear_depth); } bool write_fragment(psprecomp::GuestMemory &memory, const std::array &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(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::clamp(zf, 0.0f, 65535.0f)); 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(y) * setup.framebuffer_stride + static_cast(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(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(ca) * a.inv_w + l1 * static_cast(cb) * b.inv_w + l2 * static_cast(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(color.r) | (static_cast(color.g) << 8u) | (static_cast(color.b) << 16u) | (static_cast(color.a) << 24u); } std::uint32_t pack_gpu_alpha_control(const GeGpuDrawDescriptor &draw) noexcept { return static_cast(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(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(48u + (hash & 0xCFu)), static_cast(48u + ((hash >> 8u) & 0xCFu)), static_cast(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 { 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(72u + (hash & 0xB7u)), static_cast(72u + ((hash >> 8u) & 0xB7u)), static_cast(72u + ((hash >> 16u) & 0xB7u)), 255u, }; 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((static_cast(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; 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 &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); static thread_local std::vector 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) { } 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 (...) { } } 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); } 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 triangles{{p00, p10, p11, p00, p11, p01}}; ge_gpu_backend_accumulate_color_triangles(effective_draw, triangles); } bool prepare_screen_triangle(const std::array &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::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::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::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::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 &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 || !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 &commands, const FragmentSetup &setup, const Vertex &a, const Vertex &b, const Vertex &c, const Vertex &provoking, bool through, GeRenderStats &stats, std::vector &prepared, bool gpu_only = false) { 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; 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 &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(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(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); } } } struct DeferredRasterBatch { std::array commands{}; FragmentSetup setup{}; std::size_t first{}; std::size_t count{}; }; std::vector g_deferred_batches; std::vector g_deferred_triangles; std::int32_t g_deferred_min_y = 0; std::int32_t g_deferred_max_y = 0; bool deferred_rasterization_enabled() noexcept { static const bool enabled = std::getenv("LCS_GE_NO_DEFER") == nullptr; return enabled; } void rasterize_prepared_triangles(psprecomp::GuestMemory &memory, const std::array &commands, const FragmentSetup &setup, const std::vector &triangles, GeRenderStats &stats) { if (triangles.empty()) return; if (software_raster_skipped(commands)) return; if (deferred_rasterization_enabled()) { if (ge_phase_diag_enabled()) g_ge_triangle_count += triangles.size(); if (g_deferred_triangles.empty()) { g_deferred_min_y = triangles.front().min_y; g_deferred_max_y = triangles.front().max_y; } DeferredRasterBatch &batch = g_deferred_batches.emplace_back(); batch.commands = commands; batch.setup = setup; batch.first = g_deferred_triangles.size(); batch.count = triangles.size(); for (const PreparedScreenTriangle &triangle : triangles) { g_deferred_min_y = std::min(g_deferred_min_y, triangle.min_y); g_deferred_max_y = std::max(g_deferred_max_y, triangle.max_y); g_deferred_triangles.push_back(triangle); } 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(triangle.max_x - triangle.min_x + 1) * static_cast(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 partial{}; const auto run_parallel = [&] { pool.run(min_y, max_y, [&](unsigned index, std::int32_t row_first, std::int32_t row_last) { 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 flush_deferred_batches(psprecomp::GuestMemory &memory) { if (g_deferred_triangles.empty()) { g_deferred_batches.clear(); return; } RowWorkerPool &pool = RowWorkerPool::instance(); const std::int32_t min_y = g_deferred_min_y; const std::int32_t max_y = g_deferred_max_y; const auto rasterize_band = [&](std::int32_t row_first, std::int32_t row_last, GeRenderStats &target) { for (const DeferredRasterBatch &batch : g_deferred_batches) { for (std::size_t index = 0u; index < batch.count; ++index) { const PreparedScreenTriangle &triangle = g_deferred_triangles[batch.first + index]; if (row_last < triangle.min_y || row_first > triangle.max_y) continue; rasterize_prepared_triangle_rows(memory, batch.commands, batch.setup, triangle, std::max(row_first, triangle.min_y), std::min(row_last, triangle.max_y), target); } } }; const auto run = [&] { if (pool.worker_count() <= 1u || min_y >= max_y) { GeRenderStats discarded{}; rasterize_band(min_y, max_y, discarded); return; } std::array partial{}; pool.run(min_y, max_y, [&](unsigned index, std::int32_t row_first, std::int32_t row_last) { rasterize_band(row_first, row_last, partial[index]); }); }; if (ge_phase_diag_enabled()) { PixelLoopTimer timer; run(); } else { run(); } g_deferred_batches.clear(); g_deferred_triangles.clear(); } void rasterize_triangle(psprecomp::GuestMemory &memory, const std::array &commands, const FragmentSetup &setup, const Vertex &a, const Vertex &b, const Vertex &c, const Vertex &provoking, bool through, GeRenderStats &stats) { std::vector 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 &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::floor(vertex.x)), static_cast(std::floor(vertex.y)), vertex.z, vertex.u / q, vertex.v / q, vertex.color, stats); } void rasterize_line(psprecomp::GuestMemory &memory, const std::array &commands, const FragmentSetup &setup, Vertex a, Vertex b, bool through, GeRenderStats &stats) { if (!through) { 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::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(i) / static_cast(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::floor(a.x + dx * t)), static_cast(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; } } 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(element) * 2u; return static_cast(raw[o]) | (static_cast(raw[o + 1u]) << 8u); } const std::size_t o = static_cast(element) * 4u; return static_cast(raw[o]) | (static_cast(raw[o + 1u]) << 8u) | (static_cast(raw[o + 2u]) << 16u) | (static_cast(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::max() / width) bytes = static_cast(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; } } struct HudBox { float x0, y0, x1, y1; }; bool hud_diag_enabled() noexcept { static const bool enabled = std::getenv("LCS_HUD_DIAG") != nullptr; return enabled; } void log_hud_group(std::uint32_t primitive, const HudBox &box, float anchor_x, float anchor_y, float width, float height) { static std::mutex mutex; static std::vector seen; const auto q = [](float value) { return static_cast(std::clamp(value, -1024.0f, 1023.0f) + 1024.0f) & 0x7FFu; }; const std::uint64_t key = q(box.x0) | (q(box.y0) << 11u) | (q(box.x1) << 22u) | (q(box.y1) << 33u) | (static_cast(primitive) << 44u); const std::lock_guard lock(mutex); if (seen.size() >= 2000u || std::find(seen.begin(), seen.end(), key) != seen.end()) return; seen.push_back(key); std::fprintf(stderr, "[hud] prim=%u box=(%.1f,%.1f)-(%.1f,%.1f) anchor=(%.1f,%.1f) extent=%.1fx%.1f\n", primitive, box.x0, box.y0, box.x1, box.y1, anchor_x, anchor_y, width, height); } void scale_hud_vertices(std::vector &vertices, std::uint32_t primitive, float width, float height, float scale, float full_fraction, FragmentSetup &setup, GeGpuDrawDescriptor &gpu_draw) { const std::size_t unit = primitive == 6u ? 2u : primitive == 3u ? 3u : vertices.size(); const std::size_t units = (vertices.size() + unit - 1u) / unit; if (units == 0u) return; static thread_local std::vector boxes; static thread_local std::vector parent; boxes.resize(units); parent.resize(units); for (std::size_t u = 0u; u < units; ++u) { const std::size_t begin = u * unit; const std::size_t end = std::min(begin + unit, vertices.size()); HudBox box{vertices[begin].x, vertices[begin].y, vertices[begin].x, vertices[begin].y}; for (std::size_t i = begin + 1u; i < end; ++i) { box.x0 = std::min(box.x0, vertices[i].x); box.y0 = std::min(box.y0, vertices[i].y); box.x1 = std::max(box.x1, vertices[i].x); box.y1 = std::max(box.y1, vertices[i].y); } boxes[u] = box; parent[u] = static_cast(u); } const auto find = [&](std::uint32_t value) { while (parent[value] != value) { parent[value] = parent[parent[value]]; value = parent[value]; } return value; }; constexpr std::size_t kMaxGroupedUnits = 1024u; if (units > 1u && units <= kMaxGroupedUnits) { for (std::size_t a = 0u; a < units; ++a) { const HudBox &left = boxes[a]; for (std::size_t b = a + 1u; b < units; ++b) { const HudBox &right = boxes[b]; const float gap = std::min(left.y1 - left.y0, right.y1 - right.y0); if (left.y0 > right.y1 + 1.0f || right.y0 > left.y1 + 1.0f) continue; if (left.x0 > right.x1 + gap || right.x0 > left.x1 + gap) continue; const std::uint32_t root_a = find(static_cast(a)); const std::uint32_t root_b = find(static_cast(b)); if (root_a != root_b) parent[root_b] = root_a; } } } static thread_local std::vector groups; groups.resize(units); for (std::size_t u = 0u; u < units; ++u) { if (find(static_cast(u)) == u) groups[u] = boxes[u]; } for (std::size_t u = 0u; u < units; ++u) { const std::uint32_t root = find(static_cast(u)); if (root == u) continue; HudBox &group = groups[root]; group.x0 = std::min(group.x0, boxes[u].x0); group.y0 = std::min(group.y0, boxes[u].y0); group.x1 = std::max(group.x1, boxes[u].x1); group.y1 = std::max(group.y1, boxes[u].y1); } const auto anchor = [](float low, float high, float extent) { const float center = (low + high) * 0.5f; if (center < extent / 3.0f) return 0.0f; if (center > extent * 2.0f / 3.0f) return extent; return extent * 0.5f; }; bool single_anchor = true; bool any_scaled = false; float first_x = 0.0f; float first_y = 0.0f; for (std::size_t u = 0u; u < units; ++u) { const HudBox &group = groups[find(static_cast(u))]; if (group.x1 - group.x0 >= width * full_fraction || group.y1 - group.y0 >= height * full_fraction) { single_anchor = false; continue; } const float anchor_x = anchor(group.x0, group.x1, width); const float anchor_y = anchor(group.y0, group.y1, height); if (hud_diag_enabled() && find(static_cast(u)) == u) log_hud_group(primitive, group, anchor_x, anchor_y, width, height); if (!any_scaled) { first_x = anchor_x; first_y = anchor_y; any_scaled = true; } else if (anchor_x != first_x || anchor_y != first_y) { single_anchor = false; } const std::size_t begin = u * unit; const std::size_t end = std::min(begin + unit, vertices.size()); for (std::size_t i = begin; i < end; ++i) { vertices[i].x = anchor_x + (vertices[i].x - anchor_x) * scale; vertices[i].y = anchor_y + (vertices[i].y - anchor_y) * scale; } } if (!any_scaled || !single_anchor) return; const auto scale_scissor = [&](std::int32_t value, float origin, bool leading) { const float moved = origin + (static_cast(value) - origin) * scale; return static_cast(leading ? std::floor(moved) : std::ceil(moved)); }; setup.scissor_x0 = scale_scissor(setup.scissor_x0, first_x, true); setup.scissor_x1 = scale_scissor(setup.scissor_x1, first_x, false); setup.scissor_y0 = scale_scissor(setup.scissor_y0, first_y, true); setup.scissor_y1 = scale_scissor(setup.scissor_y1, first_y, false); gpu_draw.scissor_x0 = setup.scissor_x0; gpu_draw.scissor_x1 = setup.scissor_x1; gpu_draw.scissor_y0 = setup.scissor_y0; gpu_draw.scissor_y1 = setup.scissor_y1; } } bool test_ge_bounding_box(const psprecomp::GuestMemory &memory, const std::array &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(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 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(region1 & 0x3FFu); const float y1 = static_cast((region1 >> 10u) & 0x3FFu); const float x2 = static_cast(region2 == 0u ? 1023u : region2 & 0x3FFu); const float y2 = static_cast(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 &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) { static thread_local std::uint64_t cached_draw_revision = std::numeric_limits::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(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; const std::uint32_t gpu_scissor1 = data24(commands[0xD4u]); const std::uint32_t gpu_scissor2 = data24(commands[0xD5u]); gpu_draw.scissor_x0 = static_cast(gpu_scissor1 & 0x3FFu); gpu_draw.scissor_y0 = static_cast((gpu_scissor1 >> 10u) & 0x3FFu); gpu_draw.scissor_x1 = gpu_scissor2 == 0u ? static_cast(gpu_draw.framebuffer_stride - 1u) : static_cast(gpu_scissor2 & 0x3FFu); gpu_draw.scissor_y1 = gpu_scissor2 == 0u ? 271 : static_cast((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; gpu_draw.clut_checksum = 0u; if (gpu_draw.texture_format >= 4u && gpu_draw.texture_format <= 7u && gpu_draw.clut_address != 0u) { 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(clut[offset]) | (static_cast(clut[offset + 1u]) << 8u) | (static_cast(clut[offset + 2u]) << 16u) | (static_cast(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(clut[offset + 1u]) << 8u; checksum ^= tail; checksum *= 16777619u; } gpu_draw.clut_checksum = checksum; } } 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(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.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) { const std::array cloud_viewport{ decode_float24(data24(commands[0x42u])), decode_float24(data24(commands[0x43u])), decode_float24(data24(commands[0x45u])), decode_float24(data24(commands[0x46u])), static_cast(data24(commands[0x4Cu]) & 0xFFFFu) / 16.0f, static_cast(data24(commands[0x4Du]) & 0xFFFFu) / 16.0f}; const std::array cloud_camera_position{}; ge_gpu_backend_observe_camera(transform.view, transform.projection, cloud_viewport, cloud_camera_position, gpu_draw, count); } } } 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(count) * isize)) { error = "GE index stream lies outside guest memory"; return false; } 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; const bool gpu_only_triangle_path = gpu_backend_enabled && software_raster_skipped(commands); 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(8u, gpu_draw.texture_max_level + 1u) : 1u); std::array 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(mip_setups[level].width) * mip_setups[level].height * 4ull; if (bytes > std::numeric_limits::max() - total_bytes) { all = false; break; } total_bytes += static_cast(bytes); } std::vector 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(mip_setups[level].width) * mip_setups[level].height * 4u; if (!decode_texture_rgba_into(memory, mip_setups[level], std::span(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, {}); } } 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; 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) { 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 occurrence_indices; static thread_local std::vector unique_indices; static thread_local std::vector occurrence_remap; static thread_local std::vector decoded_vertices; static thread_local std::vector submitted_vertices; static thread_local std::vector triangle_indices; occurrence_indices.clear(); unique_indices.clear(); occurrence_remap.clear(); decoded_vertices.clear(); submitted_vertices.clear(); triangle_indices.clear(); 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::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(upper) - lower + 1u; const bool compact_range = range64 != 0u && range64 <= 65536u && range64 <= std::max(64u, static_cast(count) * 2u); if (compact_range) { contiguous_decode = true; contiguous_first = lower; contiguous_count = static_cast(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(found - unique_indices.begin())); } } } } const std::size_t decode_count = contiguous_decode ? static_cast(contiguous_count) : unique_indices.size(); 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; 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(vertex_address) + static_cast(contiguous_first) * layout.stride; const std::uint64_t byte_count64 = static_cast(decode_count) * layout.stride; if (first_address64 <= std::numeric_limits::max() && byte_count64 <= std::numeric_limits::max()) { contiguous_raw_bytes = static_cast(byte_count64); contiguous_raw = memory.raw_pointer( static_cast(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); std::array gpu_light_mul{1.0f, 1.0f, 1.0f, 1.0f}; std::array 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); 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(1u, logical_primitive_count); hw.unique_vertices_decoded = static_cast(decode_count); hw.index_reuses = count > decode_count ? count - static_cast(decode_count) : 0u; 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(i); if (contiguous_decode) return occurrence_indices[i] - contiguous_first; return occurrence_remap[i]; }; bool needs_indices = true; 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) { 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( reinterpret_cast(fast_0115_raw), fast_0115_bytes), static_cast(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(); } 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(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(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::numeric_limits::max()); if (parallel_decode) { std::atomic decode_failed{false}; std::array decode_errors{}; const std::size_t vertices_per_participant = expensive_vertex ? 64u : 128u; const unsigned useful_participants = std::max(2u, std::min( std::min(decode_pool.worker_count(), parallel_vertex_decode_max_participants()), static_cast((decode_count + vertices_per_participant - 1u) / vertices_per_participant))); decode_pool.run(0, static_cast(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(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(rgba & 0xFFu), static_cast((rgba >> 8u) & 0xFFu), static_cast((rgba >> 16u) & 0xFFu), static_cast((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; vertex.texture_env = effective_draw.texture_env; vertex.fog_control = fog_control; vertex.transform_control = transform_control; return vertex; }; if (!flat_shading) { for (GeGpuVertex &vertex : decoded_vertices) vertex = finalize_vertex(vertex); 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(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(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 upload_vertices = flat_shading ? std::span(submitted_vertices) : std::span(decoded_vertices); 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; } FragmentSetup setup = make_fragment_setup_cached(commands); { PhaseTimer bind_timer(g_ge_draw_setup_ns); bind_fragment_buffers(setup, memory, commands); } static thread_local std::vector 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); } } 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); constexpr float kFullWidthFraction = 0.9f; const bool full_width = (max_x - min_x) * hud.display_scale_x >= kFullWidthFraction * 480.0f; const bool widescreen_surface_primitive = primitive >= 3u && primitive <= 6u; const bool hud_candidate = widescreen_surface_primitive && !setup.clear_mode && !(setup.depth_test_enabled && !setup.depth_write_enabled) && !(setup.texture_enabled && ge_gpu_backend_is_framebuffer_feedback_texture(gpu_draw)); const float hud_scale = lcs_hud_scale(); if (hud_candidate && hud_scale < 1.0f && (lcs_camera_in_use() || !lcs_camera_hook_enabled())) { std::uint32_t logical_width = 480u; std::uint32_t logical_height = 272u; ge_gpu_backend_display_logical_size(logical_width, logical_height); scale_hud_vertices(vertices, primitive, static_cast(logical_width), static_cast(logical_height), hud_scale, kFullWidthFraction, setup, gpu_draw); } if (hud_candidate && !full_width && hud.shrink != 1.0f) { for (Vertex &vertex : vertices) vertex.x = hud.source_center + (vertex.x - hud.source_center) / hud.shrink; const auto shrink_scissor = [&](std::int32_t value, bool leading) { const float moved = hud.source_center + (static_cast(value) - hud.source_center) / hud.shrink; return static_cast(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; } } 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 gpu_vertices; gpu_vertices.reserve(vertices.size()); for (const Vertex &vertex : vertices) { const std::uint32_t rgba = static_cast(vertex.color.r) | (static_cast(vertex.color.g) << 8u) | (static_cast(vertex.color.b) << 16u) | (static_cast(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 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 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 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; } void flush_ge_deferred_rasterization(psprecomp::GuestMemory &memory) { flush_deferred_batches(memory); } 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; } }