Files
2026-08-13 16:21:22 -03:00

601 lines
28 KiB
C++

#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include <span>
#include <string>
#include <vector>
namespace vcs {
enum class GeGpuBackendKind : std::uint8_t {
Software,
DirectX12,
};
struct GeGpuDrawDescriptor {
std::uint32_t primitive{};
std::uint32_t vertex_count{};
std::uint32_t vertex_type{};
// GE render target for this draw. Stage 33 uses it to exclude prelight,
// reflection and other offscreen passes from the presented framebuffer.
std::uint32_t framebuffer_address{};
std::uint32_t framebuffer_stride{};
std::uint32_t framebuffer_format{};
std::uint32_t texture_format{};
std::uint32_t texture_address{};
std::uint32_t texture_buffer_width{};
std::uint32_t texture_width{};
std::uint32_t texture_height{};
std::uint32_t texture_function{};
bool texture_use_alpha{};
bool texture_double_color{};
std::uint32_t texture_env{};
std::uint32_t clut_address{};
std::uint32_t clut_format{};
std::uint32_t clut_shift{};
std::uint32_t clut_mask{};
std::uint32_t clut_start{};
// Checksum of the palette bytes themselves. The cache keys carry the CLUT
// *address*, and VCS recolours vehicles by loading a new palette into the
// same address: two cars of different colours collided on one key, so
// whichever decoded first decided the paint and the colour flipped as the
// cache refreshed or evicted. That is the blinking bodywork, and the reason
// the software rasterizer -- which decodes the palette on every draw and
// caches nothing -- never showed it.
std::uint32_t clut_checksum{};
// Lightweight signature of the source texel bytes. It is deliberately NOT
// part of the cache key: when a PSP title streams new pixels into the same
// address/layout, the existing VkImage must be refreshed rather than minting
// a permanently new cache identity.
std::uint64_t texture_content_signature{};
// Stage 42 host-only derived keys. The PSP state above is hashed once after
// the per-draw CLUT checksum is known; texture lookup/signature/upload and
// accumulation then reuse the key instead of re-hashing 8 mip descriptors
// and sampler state several times for the same PRIM. Zero means "compute".
std::uint64_t texture_cache_key_hint{};
std::uint64_t texture_image_key_hint{};
bool texture_swizzled{};
// Full PSP GE filter/mipmap state. texture_linear remains the legacy alias
// for magnification filtering so older probes and packages stay source-compatible.
bool texture_linear{};
bool texture_min_linear{};
bool texture_mag_linear{};
bool texture_mipmap_enabled{};
bool texture_mipmap_linear{};
std::uint32_t texture_max_level{};
std::uint32_t texture_level_mode{};
std::int32_t texture_level_offset16{};
float texture_lod_slope{};
std::uint32_t texture_selected_level{};
std::array<std::uint32_t, 8> texture_level_addresses{};
std::array<std::uint32_t, 8> texture_level_buffer_widths{};
std::array<std::uint32_t, 8> texture_level_widths{};
std::array<std::uint32_t, 8> texture_level_heights{};
bool texture_clamp_u{};
bool texture_clamp_v{};
// GE scissor rectangle, in PSP 480x272 screen space, inclusive on both ends.
// The Vulkan path used to scissor to the whole target, so every 2D element
// the game clips with this register -- the radar map above all -- spilled
// across the screen.
std::int32_t scissor_x0{};
std::int32_t scissor_y0{};
std::int32_t scissor_x1{479};
std::int32_t scissor_y1{271};
bool through{};
// Set by the GE renderer when it has already shrunk this draw's vertices
// back into the 16:9 box. The backend reads it to move the scissor with
// them. See ge_gpu_backend_widescreen_hud().
bool widescreen_hud{};
bool texture_enabled{};
bool blend_enabled{};
std::uint32_t blend_equation{};
std::uint32_t blend_source_factor{};
std::uint32_t blend_dest_factor{};
std::uint32_t blend_fix_source{};
std::uint32_t blend_fix_dest{};
std::uint32_t color_write_mask{};
bool alpha_test_enabled{};
std::uint32_t alpha_function{};
std::uint32_t alpha_reference{};
std::uint32_t alpha_mask{};
bool depth_test_enabled{};
bool depth_write_enabled{};
std::uint32_t depth_function{};
bool fog_enabled{};
std::uint32_t fog_color{};
float fog_end{};
float fog_slope{};
bool clear_mode{};
bool clear_color{};
bool clear_alpha{};
bool clear_depth{};
};
// Stable host-side vertex format for the first Vulkan bring-up path. Values
// are the already decoded GE coordinates/colors produced by ge_renderer.cpp;
// no guest-memory format assumptions leak into the backend.
// Per-draw hardware-transform state for the Vulkan geometry frontend.
// PSP vertex format decode stays on the CPU in this hardware-transform path,
// but does not materialize clipped/screen-space
// triangles. The backend folds viewport/composition into model_to_clip once per
// draw and pushes the final constants to the vertex shader.
struct GeGpuHardwareTransform {
std::array<float, 16> model_to_clip{};
std::array<float, 4> model_to_view_z{};
float viewport_scale_x{};
float viewport_scale_y{};
float viewport_scale_z{};
float viewport_center_x{};
float viewport_center_y{};
float viewport_center_z{};
float viewport_offset_x{};
float viewport_offset_y{};
float uv_scale_u{1.0f};
float uv_scale_v{1.0f};
float uv_offset_u{};
float uv_offset_v{};
float fog_end{};
float fog_slope{};
bool depth_clip_enabled{};
bool cull_enabled{};
bool accept_counter_clockwise{};
// Stage 45.5: preserve the PSP primitive topology on the native GPU path.
// 3 = triangle list, 4 = triangle strip, 5 = fan (fan is expanded).
std::uint32_t primitive{3u};
// Optional affine vertex-lighting transform. For the dominant 0x0115
// layout the normal is constant per draw; when every enabled light is
// directional the complete PSP lighting equation is affine in vertex
// colour. Applying it in the VS keeps the original 10-byte stream on GPU.
bool vertex_color_affine{};
std::array<float, 4> vertex_color_mul{1.0f, 1.0f, 1.0f, 1.0f};
std::array<float, 4> vertex_color_add{};
std::uint32_t logical_prim_batches{1u};
std::uint32_t unique_vertices_decoded{};
std::uint32_t index_reuses{};
};
struct GeGpuVertex {
float x{};
float y{};
float z{};
float w{1.0f};
std::uint32_t rgba{0xFFFFFFFFu};
float u{};
float v{};
// Packed as R8G8B8A8_UINT: enable, function, reference, mask.
std::uint32_t alpha_control{0xFF000100u};
// Packed as R8G8B8A8_UINT: function, use-alpha, double-color, reserved.
std::uint32_t texture_control{};
// Packed as R8G8B8A8_UINT. RGB is the PSP GE texture-environment color;
// A is reserved. Required by GU_TFX_BLEND and carried flat per primitive.
std::uint32_t texture_env{};
// Perspective-interpolated PSP fog coefficient in [0, 1]. 1 preserves the
// fragment color; 0 selects the flat fog color.
float fog_factor{1.0f};
// Packed R8G8B8A8_UINT: RGB fog color, A nonzero when fog is enabled.
std::uint32_t fog_control{};
// PSP projective texture denominator. The hardware interpolates U, V and Q
// perspective-correctly, then the fragment stage samples at (U/Q, V/Q).
float q{1.0f};
// bit 0 = model-space/HW transform, bit 1 = culling enabled,
// bit 2 = PSP accepts counter-clockwise faces, bit 3 = depth clip enabled.
std::uint32_t transform_control{};
};
struct GeGpuDecodedMipLevel {
std::uint32_t width{};
std::uint32_t height{};
std::span<const std::byte> rgba8{};
};
struct GeGpuBackendReport {
GeGpuBackendKind requested{GeGpuBackendKind::Software};
GeGpuBackendKind active{GeGpuBackendKind::Software};
bool strict{};
bool loader_opened{};
bool instance_created{};
bool device_created{};
bool transfer_buffer_created{};
bool transfer_memory_mapped{};
bool command_pool_created{};
bool transfer_self_test_passed{};
bool offscreen_image_created{};
bool offscreen_image_memory_bound{};
bool offscreen_image_view_created{};
bool render_pass_created{};
bool framebuffer_created{};
bool shader_modules_created{};
bool graphics_pipeline_created{};
bool offscreen_self_test_passed{};
std::uint32_t physical_device_count{};
std::uint32_t graphics_queue_family{0xFFFFFFFFu};
std::uint32_t memory_type_index{0xFFFFFFFFu};
std::uint64_t draw_calls{};
std::uint64_t vertices{};
std::uint64_t textured_draw_calls{};
std::uint64_t unique_pipeline_keys{};
std::uint64_t unique_texture_keys{};
// Distinct decoded images behind those keys. The gap between the two is the
// share of the texture cache that is duplicate art differing only in sampler
// state (filtering, wrap, LOD).
std::uint64_t unique_texture_image_keys{};
// Cache entries that adopted an image another entry had already decoded and
// uploaded, instead of creating and filling a duplicate.
std::uint64_t shared_texture_images{};
std::uint64_t captured_draws{};
std::uint64_t draw_ring_capacity{};
std::uint64_t draw_ring_overwrites{};
std::uint64_t upload_capacity_bytes{};
// Complete host frame resource sets available for CPU/GPU overlap.
std::uint32_t frames_in_flight_capacity{1u};
std::uint64_t staged_draw_calls{};
std::uint64_t staged_vertices{};
std::uint64_t staged_bytes{};
std::uint64_t upload_wraps{};
std::uint64_t transfer_submissions{};
std::uint64_t transfer_bytes{};
// Performance diagnostics only. These counters are populated only when
// PSPRECOMP_GPU_TIMING_DIAG is set; they do not change renderer policy.
std::uint64_t perf_wait_for_frame_calls{};
std::uint64_t perf_wait_for_frame_ns{};
std::uint64_t perf_upload_flush_wait_calls{};
std::uint64_t perf_upload_flush_wait_ns{};
std::uint64_t perf_acquire_calls{};
std::uint64_t perf_acquire_ns{};
std::uint64_t perf_queue_submit_calls{};
std::uint64_t perf_queue_submit_ns{};
std::uint64_t perf_queue_present_calls{};
std::uint64_t perf_queue_present_ns{};
std::uint64_t perf_finish_frame_calls{};
std::uint64_t perf_finish_frame_ns{};
std::uint64_t rejected_gpu_draws{};
std::uint32_t offscreen_width{};
std::uint32_t offscreen_height{};
std::uint32_t offscreen_center_rgba{};
std::uint64_t offscreen_draw_calls{};
std::uint64_t offscreen_readback_bytes{};
std::uint64_t offscreen_changed_pixels{};
std::uint64_t offscreen_checksum{};
// Stage 24: actual VCS screen-space triangles rasterized into the configured
// internal-resolution Vulkan target. Textures/depth are not authoritative
// yet, so this remains a diagnostic color-geometry preview.
std::uint64_t game_frames{};
std::uint64_t game_draw_calls{};
std::uint64_t game_triangles{};
std::uint64_t game_vertices{};
std::uint64_t hw_transform_draw_calls{};
std::uint64_t hw_transform_vertices{};
std::uint64_t hw_transform_prim_batches{};
std::uint64_t hw_transform_unique_vertices_decoded{};
std::uint64_t hw_transform_index_reuses{};
std::uint64_t game_textured_draws_without_texture{};
std::uint64_t game_vertex_overflows{};
std::uint64_t game_frame_readback_bytes{};
std::uint64_t game_frame_changed_pixels{};
std::uint64_t game_frame_checksum{};
std::uint64_t game_frame_vblank{};
// Stage 25: exact CPU decode of PSP indexed textures before descriptor-backed
// sampling lands. T4/T8 + CLUT data are cached as RGBA8 and staged into the
// persistent upload ring so the next Vulkan stage can create images without
// touching guest memory again.
std::uint64_t texture_decode_requests{};
std::uint64_t texture_cache_hits{};
std::uint64_t decoded_texture_uploads{};
std::uint64_t decoded_texture_bytes{};
std::uint64_t decoded_t4_textures{};
std::uint64_t decoded_t8_textures{};
std::uint64_t texture_images_created{};
std::uint64_t texture_image_uploads{};
std::uint64_t texture_image_upload_bytes{};
// Stage 26: descriptor-backed sampling of the Stage 25 RGBA8 VkImages.
bool texture_descriptor_layout_created{};
bool texture_descriptor_pool_created{};
bool textured_shader_modules_created{};
bool textured_pipeline_created{};
// Stage 27: D16 depth attachment plus PSP GE compare/write state.
bool depth_image_created{};
bool depth_image_memory_bound{};
bool depth_image_view_created{};
bool depth_attachment_active{};
std::uint64_t depth_pipeline_variants_created{};
std::uint64_t depth_tested_game_draw_calls{};
std::uint64_t depth_writing_game_draw_calls{};
// Stage 28: exact PSP GE alpha compare in color and textured fragments.
bool alpha_test_shader_active{};
std::uint64_t alpha_tested_game_draw_calls{};
// Stage 29: dominant PSP ADD/SRC_ALPHA/ONE_MINUS_SRC_ALPHA blend path.
bool standard_alpha_blend_pipeline_active{};
bool observed_blend_modes_pipeline_active{};
std::uint64_t blend_pipeline_variants_created{};
std::uint64_t standard_alpha_blended_game_draw_calls{};
std::uint64_t fixed_replace_blended_game_draw_calls{};
std::uint64_t additive_blended_game_draw_calls{};
std::uint64_t unsupported_blend_game_draw_calls{};
// Stage 31: complete PSP texture-function switch (MODULATE, DECAL, BLEND,
// REPLACE and ADD), texture environment color, all uncompressed base
// texture formats 0..7, and whole-channel color write masks.
bool observed_texture_function_shader_active{};
bool complete_texture_function_shader_active{};
bool color_write_mask_pipeline_active{};
bool base_texture_formats_active{};
std::uint64_t modulate_texture_game_draw_calls{};
std::uint64_t decal_texture_game_draw_calls{};
std::uint64_t blend_texture_game_draw_calls{};
std::uint64_t replace_texture_game_draw_calls{};
std::uint64_t add_texture_game_draw_calls{};
std::uint64_t double_color_texture_game_draw_calls{};
std::uint64_t unsupported_texture_function_game_draw_calls{};
std::uint64_t color_mask_pipeline_variants_created{};
std::uint64_t masked_color_game_draw_calls{};
std::uint64_t unsupported_partial_color_mask_game_draw_calls{};
std::uint64_t decoded_direct16_textures{};
std::uint64_t decoded_direct32_textures{};
std::uint64_t decoded_indexed16_textures{};
std::uint64_t decoded_indexed32_textures{};
// Stage 32: PSP S3TC, explicit mip-level/filter state and per-fragment fog.
bool compressed_texture_formats_active{};
bool mipmap_state_active{};
bool fog_shader_active{};
std::uint64_t decoded_dxt1_textures{};
std::uint64_t decoded_dxt3_textures{};
std::uint64_t decoded_dxt5_textures{};
std::uint64_t mipmapped_game_draw_calls{};
std::uint64_t mip_linear_game_draw_calls{};
std::uint64_t fixed_lod_game_draw_calls{};
std::uint64_t selected_nonzero_mip_game_draw_calls{};
bool full_mip_chain_active{};
std::uint64_t uploaded_mip_levels{};
std::uint64_t automatic_lod_game_draw_calls{};
std::uint64_t slope_lod_game_draw_calls{};
std::uint64_t fogged_game_draw_calls{};
// Stage 33: persistent framebuffer-feedback bridge and display-target
// selection for VCS's delayed prelight/bloom/final-composition chain.
std::uint64_t framebuffer_targets_observed{};
// Draws that sample the surface the guest is currently displaying. This is
// the empirical test for PSPRECOMP_GE_GPU_SKIP_DISPLAYED_RASTER: skipping
// the CPU rasterization of that surface is only safe if nothing reads it
// back, and a nonzero count here means something does.
std::uint64_t display_framebuffer_sampled_draws{};
// LRU eviction. evicted_textures rising while unbound_textured_draws stays
// at zero is the cache working; unbound_textured_draws rising again means
// the working set no longer fits and the ceilings need raising.
std::uint64_t evicted_textures{};
std::uint64_t recycled_texture_descriptor_sets{};
std::uint64_t vram_feedback_refreshes{};
// Stage 44.5 DX12: persistent EDRAM framebuffer surfaces live as native
// D3D12 render-target/SRV resources. Feedback draws sample them GPU->GPU;
// same-target feedback snapshots the pre-draw image on the GPU to avoid
// the D3D12 RTV+SRV hazard without a CPU readback.
std::uint64_t dx12_native_framebuffer_targets{};
std::uint64_t dx12_gpu_feedback_draws{};
std::uint64_t dx12_self_feedback_snapshots{};
// Stage 44.6 Direct3D 12 release-candidate diagnostics.
std::uint32_t dx12_msaa_samples{1u};
std::uint32_t dx12_depth_bits{32u};
std::uint64_t dx12_resolves{};
std::uint64_t dx12_device_recoveries{};
std::uint32_t presented_framebuffer_target{};
// True once the rendered image goes straight to a native GPU swapchain instead
// of being copied back to system memory and blitted with GDI.
bool swapchain_active{};
// Vblanks whose batches never touched the displayed framebuffer and were
// therefore handed back to the software path instead of being presented.
std::uint64_t frames_without_displayed_target{};
bool gpu_frame_presented_to_window{};
bool release_candidate_ready{};
std::uint64_t texture_samplers_created{};
std::uint64_t texture_descriptor_sets_allocated{};
std::uint64_t textured_game_draw_calls{};
std::uint64_t textured_game_triangles{};
std::uint64_t textured_game_vertices{};
std::uint64_t missing_texture_draw_calls{};
std::uint64_t rejected_texture_decodes{};
std::uint64_t texture_upload_wraps{};
std::uint64_t last_texture_key{};
std::uint64_t last_texture_checksum{};
std::uint32_t last_texture_width{};
std::uint32_t last_texture_height{};
std::uint32_t last_texture_format{};
std::string message;
};
// Initializes the optional persistent GPU prototype. The default remains the
// bit-exact software rasterizer. Set PSPRECOMP_GE_BACKEND=vulkan to create a
// persistent Vulkan instance/device and transfer/upload path while software
// rendering remains authoritative during bring-up.
[[nodiscard]] bool initialize_ge_gpu_backend(std::string &error);
void shutdown_ge_gpu_backend() noexcept;
[[nodiscard]] bool ge_gpu_backend_active() noexcept;
[[nodiscard]] bool ge_gpu_backend_transfer_ready() noexcept;
[[nodiscard]] bool ge_gpu_backend_graphics_ready() noexcept;
void ge_gpu_backend_record_draw(const GeGpuDrawDescriptor &draw) noexcept;
// Captures the dominant projected world camera for optional native post effects.
// The matrices are observed with the draw, before the GE state advances.
void ge_gpu_backend_observe_camera(const std::array<float, 12> &view,
const std::array<float, 16> &projection,
const std::array<float, 6> &viewport,
const std::array<float, 3> &camera_position,
const GeGpuDrawDescriptor &draw,
std::uint32_t vertex_weight) noexcept;
// Stages already-decoded vertices into a persistently mapped Vulkan buffer.
// This does not replace the software rasterizer yet; it proves that real VCS
// geometry can cross the host->Vulkan boundary with no per-draw allocation.
[[nodiscard]] bool ge_gpu_backend_stage_vertices(
const GeGpuDrawDescriptor &draw,
std::span<const GeGpuVertex> vertices) noexcept;
// Returns true when the Vulkan-side texture cache does not yet contain the
// complete PSP texture/CLUT state represented by this draw.
[[nodiscard]] bool ge_gpu_backend_texture_needed(
const GeGpuDrawDescriptor &draw) noexcept;
// Content hashing is required only once per cache entry per display interval.
// Repeated draws of the same immutable texture in one vblank can reuse the
// signature already validated by the first draw. Framebuffer feedback remains
// conservative and always requests a fresh signature.
void ge_gpu_backend_prepare_texture_keys(GeGpuDrawDescriptor &draw) noexcept;
[[nodiscard]] bool ge_gpu_backend_texture_signature_needed(
const GeGpuDrawDescriptor &draw) noexcept;
// True when the sampled texture aliases a render target observed by the GE.
// Renderer-side upload code uses this to force framebuffer feedback to one mip
// level; generated render targets do not have a valid PSP mip chain behind them.
[[nodiscard]] bool ge_gpu_backend_is_framebuffer_feedback_texture(
const GeGpuDrawDescriptor &draw) noexcept;
// Widescreen interface correction, expressed in the coordinates the GE renderer
// actually has: the draw's own render target, before the game's composition
// pass shrinks it into the 480x272 display.
//
// The correction has to be applied once, in the renderer, and not here. VCS
// renders on a two-vblank cycle: one vblank draws the interface, the next only
// replays 64 composition quads that copy an already-finished surface. The
// software rasterizer keeps filling guest VRAM for targets the GPU does not
// own, so correcting only the Vulkan vertices left two copies of the HUD --
// corrected on drawing vblanks, stretched on composition vblanks, alternating
// at 30 Hz on the loading screen.
struct GeGpuWidescreenHud {
// Divide distances from source_center by this. 1 means the correction is
// off, and every other field should be ignored.
float shrink{1.0f};
// Middle of the display, in this target's coordinates.
float source_center{240.0f};
// Target-to-display scale, so a caller can test a span against the real
// 480 px screen width without knowing the target's size.
float display_scale_x{1.0f};
};
[[nodiscard]] GeGpuWidescreenHud ge_gpu_backend_widescreen_hud(
const GeGpuDrawDescriptor &draw) noexcept;
// Records a through-mode draw's reach *before* the widescreen correction moves
// it. Whether a target counts as oversized is decided from how far its draws
// go, so feeding it shrunk coordinates would let the correction argue the
// composition mapping it depends on out of existence.
void ge_gpu_backend_note_through_extent(const GeGpuDrawDescriptor &draw,
float max_x, float max_y) noexcept;
// Creates the cache entry for this draw out of an image another entry already
// decoded, when the two differ only in sampler state. Returns false when there
// is no such image and the caller must decode.
//
// The cache key includes filtering, wrap mode and LOD, none of which change the
// decoded pixels, so the same art appears under many keys -- measured at 17
// keys per distinct image. Without this the duplicates are decoded from guest
// memory every time even though the result is thrown away.
[[nodiscard]] bool ge_gpu_backend_adopt_shared_texture(
const GeGpuDrawDescriptor &draw) noexcept;
// Returns true only after the complete texture state has a sampled VkImage,
// sampler and descriptor set ready for Stage 26 draw submission.
[[nodiscard]] bool ge_gpu_backend_texture_available(
const GeGpuDrawDescriptor &draw) noexcept;
// Stores one exact native-resolution RGBA8 decode of a T4/T8 PSP texture in the
// persistent cache and stages the bytes through the mapped Vulkan upload ring.
// This stage does not sample the image in the fragment shader yet.
[[nodiscard]] bool ge_gpu_backend_upload_decoded_texture(
const GeGpuDrawDescriptor &draw,
std::uint32_t width, std::uint32_t height,
std::span<const std::byte> rgba8) noexcept;
[[nodiscard]] bool ge_gpu_backend_upload_decoded_texture_chain(
const GeGpuDrawDescriptor &draw,
std::span<const GeGpuDecodedMipLevel> levels) noexcept;
// Stage 40 fast path: the renderer can decode a complete mip chain directly
// into one contiguous allocation and transfer ownership here. This avoids the
// old vector-per-mip allocation followed by a second full-size pack/copy.
[[nodiscard]] bool ge_gpu_backend_upload_decoded_texture_chain_packed(
const GeGpuDrawDescriptor &draw,
std::uint32_t base_width, std::uint32_t base_height,
std::uint32_t mip_levels, std::vector<std::byte> rgba8) noexcept;
// Copies the most recently accepted decoded texture for proof/debug tooling.
[[nodiscard]] bool ge_gpu_backend_copy_last_texture_rgba(
std::span<std::byte> destination) noexcept;
// Adds already-clipped, screen-space triangle-list vertices to the current
// high-resolution Vulkan frame. Coordinates are in the PSP 480x272 viewport;
// the backend maps them to the configured internal target.
void ge_gpu_backend_accumulate_color_triangles(
const GeGpuDrawDescriptor &draw,
std::span<const GeGpuVertex> triangle_vertices) noexcept;
// Model-space triangle-list submission used by Stage 39.
void ge_gpu_backend_accumulate_hardware_triangles(
const GeGpuDrawDescriptor &draw,
const GeGpuHardwareTransform &transform,
std::span<const GeGpuVertex> vertices,
std::span<const std::uint32_t> triangle_indices) noexcept;
// Stage 45.4 DX12 fast path for the dominant VCS world vertex format
// (vtype 0x000115: u8 UV + 5551 colour + s16 XYZ, 10-byte stride).
// The renderer snapshots the untouched PSP bytes and lets the DX12 vertex
// shader decode them, removing the float/color conversion loop from the CPU.
// Returns false on backends that do not implement this exact packed format so
// the caller can fall back to ge_gpu_backend_accumulate_hardware_triangles().
[[nodiscard]] bool ge_gpu_backend_accumulate_hardware_packed_0115(
const GeGpuDrawDescriptor &draw,
const GeGpuHardwareTransform &transform,
std::span<const std::byte> packed_vertices,
std::uint32_t vertex_count,
std::span<const std::uint32_t> triangle_indices) noexcept;
// Stage 45.5 broad raw-vertex GPU decode. The source span contains a compact
// contiguous range of original PSP records described by `layout`. `indices`
// are local indices into that compact range; an empty span means non-indexed.
// Supplies the native host window used by a backend-owned swapchain. Passing
// nullptr detaches it. DX12 uses this to present the native GE render target
// directly without routing pixels back through the CPU presenter.
void ge_gpu_backend_set_native_window(void *native_window) noexcept;
// Publishes the framebuffer address the guest is currently displaying
// (sceDisplaySetFrameBuf). The frame assembler needs it to tell the displayed
// surface apart from the offscreen render targets VCS also draws into, which
// otherwise alternate into the window as unrelated images.
void ge_gpu_backend_set_display_framebuffer(std::uint32_t address) noexcept;
// Finishes one real VCS GPU preview frame at vblank, submits it once, waits for
// completion and stores an RGBA8 readback. Returns true when a new frame exists.
[[nodiscard]] bool ge_gpu_backend_finish_color_frame(std::uint64_t vblank) noexcept;
[[nodiscard]] bool ge_gpu_backend_copy_game_frame_rgba(
std::span<std::byte> destination) noexcept;
// True when finished frames are presented by the backend itself through a
// native GPU swapchain. The caller must not present the window in that case.
[[nodiscard]] bool ge_gpu_backend_presents_directly() noexcept;
// Framebuffer address (masked to VRAM) whose image the GPU rendered and put on
// screen last, or 0 when the GPU owns nothing. Rasterizing that surface again
// on the CPU produces pixels no one reads.
[[nodiscard]] std::uint32_t ge_gpu_backend_owned_framebuffer() noexcept;
// Framebuffer address (masked to VRAM) the guest last handed to
// sceDisplaySetFrameBuf, or 0 when the swapchain is not presenting. While the
// backend presents its own image this surface never reaches the screen, so the
// composition VCS rasterizes into it is only needed by whatever samples it back.
[[nodiscard]] std::uint32_t ge_gpu_backend_display_framebuffer() noexcept;
// Borrowed view of the last readback, valid until the next finished frame.
// Presenting through this avoids allocating and copying a desktop-sized RGBA
// buffer on every vblank.
[[nodiscard]] std::span<const std::byte> ge_gpu_backend_game_frame_rgba() noexcept;
// Copies the cached 64x64 RGBA8 offscreen self-test result. This is intended
// for validation/proof tooling; game output still comes from the software GE.
[[nodiscard]] bool ge_gpu_backend_copy_offscreen_rgba(
std::span<std::byte> destination) noexcept;
void ge_gpu_backend_mark_window_presented() noexcept;
[[nodiscard]] GeGpuBackendReport ge_gpu_backend_report();
[[nodiscard]] const char *ge_gpu_backend_name(GeGpuBackendKind kind) noexcept;
} // namespace vcs