diff --git a/CMakeLists.txt b/CMakeLists.txt index 93ea2d6..c4e2c6d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -228,6 +228,7 @@ set(SKATE3_COMMON_SOURCES src/skate3_fov.cpp src/skate3_iso_installer.cpp src/skate3_native_render.cpp + src/skate3_native_scene.cpp src/skate3_title_update_installer.cpp src/skate3_user_settings.cpp third_party/rexglue-sdk/thirdparty/crypto/sha256.cpp @@ -257,6 +258,7 @@ target_include_directories(skate3 PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_BINARY_DIR}/include + ${REXSDK_DIR}/thirdparty/xxHash ) target_compile_definitions(skate3 PRIVATE SKATE3_BUILD_CONFIG="$") if(NOT SKATE3_EFFECTIVE_TITLE_UPDATE_PACKAGE STREQUAL "") @@ -293,7 +295,7 @@ if(APPLE) endif() if(WIN32) - target_link_libraries(skate3 PRIVATE comdlg32 winhttp) + target_link_libraries(skate3 PRIVATE comdlg32 winhttp d3dcompiler) endif() add_custom_target(skate3_stage_runtime_library diff --git a/src/skate3_native_render.cpp b/src/skate3_native_render.cpp index 5376934..b0408be 100644 --- a/src/skate3_native_render.cpp +++ b/src/skate3_native_render.cpp @@ -1,5 +1,7 @@ #include "skate3_native_render.h" +#include "skate3_native_scene.h" + #include "generated/skate3_init.h" #include @@ -47,12 +49,7 @@ namespace { // One per-mesh submission. kind 0 = RenderMesh (dynamic entities: r3 is the // MeshContext itself, b = VertexProgramState). kind 1 = SceneRenderView draw // list entry (world geometry: a = MeshContext, b = sort key, c = view). -struct RenderMeshRecord { - uint32_t kind; - uint32_t a; - uint32_t b; - uint32_t c; -}; +using RenderMeshRecord = skate3::native_scene::SubmitRecord; struct FrameRecords { uint64_t frame_index; @@ -193,13 +190,15 @@ void OnSceneDrawList(uint8_t* base, uint32_t view, uint32_t sort_vec, uint32_t f if (entries == 0) { return; } + // b = which of the view's sort lists this came from (sort_vec - view), so + // the scene builder can select the primary opaque list (+20160). + const uint32_t list_offset = sort_vec - view; std::lock_guard lock(g_mutex); for (uint32_t i = 0; i < count; ++i) { const uint32_t entry = entries + (first + i) * 8; - const uint32_t key = REX_LOAD_U32(entry); const uint32_t mesh_context = REX_LOAD_U32(entry + 4); if (mesh_context != 0) { - g_current_frame.push_back({1, mesh_context, key, view}); + g_current_frame.push_back({1, mesh_context, list_offset, view}); } } } @@ -222,6 +221,9 @@ void OnFrameEnd(uint8_t* base) { ++g_frame_index; const size_t mesh_count = g_current_frame.size(); + skate3::native_scene::BuildFrameScene(base, g_current_frame.data(), + g_current_frame.size()); + const int32_t log_interval = REXCVAR_GET(skate3_native_render_log_interval); if (log_interval > 0 && g_frame_index % static_cast(log_interval) == 0) { REXLOG_INFO("native-render frame={} meshes={} snapshot_done={}", g_frame_index, @@ -274,6 +276,7 @@ void Install() { REXCVAR_GET(skate3_native_render_snapshot_min_meshes), REXCVAR_GET(skate3_native_render_snapshot_frames)); } + skate3::native_scene::Install(); } } // namespace skate3::native_render diff --git a/src/skate3_native_scene.cpp b/src/skate3_native_scene.cpp new file mode 100644 index 0000000..0533b24 --- /dev/null +++ b/src/skate3_native_scene.cpp @@ -0,0 +1,773 @@ +#include "skate3_native_scene.h" + +#include "generated/skate3_init.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#if defined(REX_HAS_D3D12) && REX_HAS_D3D12 +#include +#include +#include +#endif + +REXCVAR_DEFINE_BOOL(skate3_native_render_scene, false, "Skate 3", + "Render the game scene natively from the hooked MeshContext stream, " + "replacing the emulated GPU output (requires skate3_native_render)") + .lifecycle(rex::cvar::Lifecycle::kRequiresRestart); +REXCVAR_DEFINE_INT32(skate3_native_render_scene_debug, 0, "Skate 3", + "Native scene debug: 0=normal, 1=clear only, 2=solid color per item, " + "3=limit to 20 items, 4=depth test disabled") + .range(0, 4) + .lifecycle(rex::cvar::Lifecycle::kHotReload); + +namespace skate3::native_scene { +namespace { + +// Verified guest structure offsets. +constexpr uint32_t kCtxMatrix = 0x10; +constexpr uint32_t kCtxDrawCountU16 = 0x38; +constexpr uint32_t kCtxDrawList = 0x48; +constexpr uint32_t kMeshMaterial = 0x24; +constexpr uint32_t kMeshVertexDescriptor = 0x28; +constexpr uint32_t kMeshIndexBuffer = 0x30; +constexpr uint32_t kMeshVertexBuffer = 0x34; +constexpr uint32_t kBufferPhysAddr = 0x18; +constexpr uint32_t kVbBytes = 0x20; +constexpr uint32_t kIbCount = 0x20; +constexpr uint32_t kViewCameraFromView = 0x08; +constexpr uint32_t kViewCamViewProj = 0xA0; + +std::mutex g_scene_mutex; +FrameScene g_scene; +uint64_t g_generation = 0; +std::atomic g_guest_base{nullptr}; +std::atomic g_frames_rendered{0}; + +bool SceneEnabled() { return REXCVAR_GET(skate3_native_render_scene); } + +float LoadGuestF32(uint8_t* base, uint32_t addr) { + const uint32_t bits = REX_LOAD_U32(addr); + return std::bit_cast(bits); +} + +bool GuestReadableApprox(uint8_t* base, uint32_t addr) { + // The hook layer only walks pointers the game is actively rendering from; + // they are mapped. Reject null/small. + (void)base; + return addr >= 0x10000; +} + +// Walk one MeshContext into a DrawItem. Returns false if any pointer in the +// chain is implausible. use_matrix: world geometry has absolute coordinates +// and its ctx+0x10 points into a transient per-frame matrix arena; reading +// it late picks up recycled garbage, so world items force identity. +bool BuildItem(uint8_t* base, uint32_t ctx, bool use_matrix, DrawItem& item) { + const uint32_t record = REX_LOAD_U32(ctx); + if (!GuestReadableApprox(base, record)) return false; + const uint32_t mesh = REX_LOAD_U32(record); + if (!GuestReadableApprox(base, mesh)) return false; + + const uint32_t vdesc = REX_LOAD_U32(mesh + kMeshVertexDescriptor); + const uint32_t ib = REX_LOAD_U32(mesh + kMeshIndexBuffer); + const uint32_t vb = REX_LOAD_U32(mesh + kMeshVertexBuffer); + if (!GuestReadableApprox(base, vdesc) || !GuestReadableApprox(base, ib) || + !GuestReadableApprox(base, vb)) { + return false; + } + + // Vertex descriptor: find the stream-0 position element. + const uint32_t num_elements = REX_LOAD_U16(vdesc + 8); + if (num_elements == 0 || num_elements > 32) return false; + bool have_pos = false; + for (uint32_t i = 0; i < num_elements; ++i) { + const uint32_t e = vdesc + 0x10 + i * 16; + const uint32_t stream = REX_LOAD_U16(e); + const uint32_t usage = REX_LOAD_U8(e + 9); + if (stream == 0 && usage == 0) { + item.pos_offset = REX_LOAD_U16(e + 2); + item.pos_fmt = uint8_t(REX_LOAD_U32(e + 4) & 0x3F); + have_pos = true; + break; + } + } + if (!have_pos) return false; + item.stride = REX_LOAD_U8(vdesc + (num_elements + 1) * 16); + if (item.stride == 0) return false; + + // Skip the backdrop/ocean/sky class (multi-km bounds). The camera sits + // inside these volumes, their near-plane-crossing triangles rasterize as + // full-screen cover at depth ~0, and the real game renders them with + // dedicated passes. Mesh BBox = two Vector4s at +0x00/+0x10. + for (int axis = 0; axis < 3; ++axis) { + const float lo = LoadGuestF32(base, mesh + axis * 4); + const float hi = LoadGuestF32(base, mesh + 0x10 + axis * 4); + if (!(hi - lo < 1500.0f)) { + return false; + } + item.bbox_min[axis] = lo; + item.bbox_max[axis] = hi; + } + + item.mesh = mesh; + item.vb_addr = REX_LOAD_U32(vb + kBufferPhysAddr) & 0xFFFFFFFC; + item.vb_bytes = REX_LOAD_U32(vb + kVbBytes); + item.ib_addr = REX_LOAD_U32(ib + kBufferPhysAddr) & 0xFFFFFFFC; + item.ib_count = REX_LOAD_U32(ib + kIbCount); + if (item.vb_addr == 0 || item.ib_addr == 0 || item.vb_bytes == 0 || + item.ib_count == 0 || item.vb_bytes % item.stride != 0) { + return false; + } + + // Culled island draw list from the context. + const uint32_t draw_count = REX_LOAD_U16(ctx + kCtxDrawCountU16); + const uint32_t draw_list = REX_LOAD_U32(ctx + kCtxDrawList); + if (draw_count == 0 || draw_count > 512 || !GuestReadableApprox(base, draw_list)) { + return false; + } + item.draws.reserve(draw_count); + for (uint32_t i = 0; i < draw_count; ++i) { + const uint32_t d = draw_list + i * 16; + DrawEntry entry{REX_LOAD_U32(d), REX_LOAD_U32(d + 4), REX_LOAD_U32(d + 8), + REX_LOAD_U32(d + 12)}; + if (entry.index_count == 0 || entry.index_count > item.ib_count) continue; + item.draws.push_back(entry); + } + if (item.draws.empty()) return false; + + // Payload fingerprint (FNV-1a over sampled VB/IB bytes + sizes) so the + // renderer re-decodes when streaming replaces the data at this address. + uint64_t h = 1469598103934665603ull; + const auto mix = [&h](uint64_t v) { + h = (h ^ v) * 1099511628211ull; + }; + mix(item.vb_bytes); + mix(item.ib_count); + const uint32_t vb_end = item.vb_addr + item.vb_bytes - 32; + const uint32_t ib_end = item.ib_addr + item.ib_count * 2 - 32; + for (uint32_t off = 0; off < 32; off += 8) { + mix(REX_LOAD_U64(item.vb_addr + off)); + mix(REX_LOAD_U64(vb_end + off)); + mix(REX_LOAD_U64(item.ib_addr + off)); + mix(REX_LOAD_U64(ib_end + off)); + } + item.fingerprint = h; + + // Transform (identity for world geometry, instance matrix for props; + // characters get their bone array's first matrix, which roughly places + // them until skinning is implemented). + const uint32_t mtx = use_matrix ? REX_LOAD_U32(ctx + kCtxMatrix) : 0; + if (mtx != 0 && GuestReadableApprox(base, mtx)) { + for (int i = 0; i < 16; ++i) { + item.world[i] = LoadGuestF32(base, mtx + i * 4); + } + } else { + std::memset(item.world, 0, sizeof(item.world)); + item.world[0] = item.world[5] = item.world[10] = item.world[15] = 1.0f; + } + return true; +} + +} // namespace + +bool Enabled() { return SceneEnabled(); } + +void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) { + if (!SceneEnabled() || count == 0) { + return; + } + g_guest_base.store(base, std::memory_order_relaxed); + + // Multiple SceneRenderViews can submit per frame (main, shadow cascades, + // reflections). Pick the perspective one (proj[2][3] == 1 in row-vector + // convention) and only take its items, deduplicated (the same context can + // appear in several of the view's sort lists). + uint32_t view = 0; + uint32_t viewcam = 0; + for (size_t i = 0; i < count; ++i) { + const SubmitRecord& r = records[i]; + if (r.kind != 1 || r.c == 0) { + continue; + } + const uint32_t cam = REX_LOAD_U32(r.c + kViewCameraFromView); + if (!GuestReadableApprox(base, cam)) { + continue; + } + const float persp_w = LoadGuestF32(base, cam + 0x60 + (2 * 4 + 3) * 4); + if (persp_w == 1.0f) { + view = r.c; + viewcam = cam; + break; + } + } + if (view == 0) { + return; + } + + FrameScene scene; + scene.items.reserve(count); + std::unordered_set seen; + for (size_t i = 0; i < count; ++i) { + const SubmitRecord& r = records[i]; + // Dynamic entities (kind 0) are skinned characters/props rendered in + // bind pose without bone matrices; the skater's limbs sprawl across the + // camera. Skip until skinning is implemented; render world geometry only. + if (r.kind == 0) { + continue; + } + // Primary opaque list of the chosen view only; other lists (shadow + // culling, transparents, z-prepass) duplicate the same geometry through + // different MeshContext objects and z-fight. + if (r.c != view || r.b != 20160) { + continue; + } + if (!seen.insert(r.a).second) { + continue; + } + DrawItem item; + if (BuildItem(base, r.a, /*use_matrix=*/r.kind == 0, item)) { + scene.items.push_back(std::move(item)); + } + } + if (scene.items.empty()) { + return; + } + for (int i = 0; i < 16; ++i) { + scene.view_proj[i] = LoadGuestF32(base, viewcam + kViewCamViewProj + i * 4); + } + // Camera position from the view matrix (+0x20, row-vector convention): + // cam = -t * R^T. + { + float view[16]; + for (int i = 0; i < 16; ++i) { + view[i] = LoadGuestF32(base, viewcam + 0x20 + i * 4); + } + for (int j = 0; j < 3; ++j) { + scene.cam_pos[j] = -(view[12] * view[j * 4 + 0] + view[13] * view[j * 4 + 1] + + view[14] * view[j * 4 + 2]); + } + } + // The game's projection uses a negative x scale which already yields + // correct D3D NDC orientation; use the view*proj matrix as captured. + // (Negating column 0 here mirrors the image left-right.) + + std::lock_guard lock(g_scene_mutex); + scene.generation = ++g_generation; + g_scene = std::move(scene); +} + +} // namespace skate3::native_scene + +#if defined(REX_HAS_D3D12) && REX_HAS_D3D12 + +namespace skate3::native_scene { +namespace { + +using rex::graphics::NativeGuestOutputBackend; +using rex::graphics::NativeGuestOutputRenderContext; + +struct MeshBuffers { + ID3D12Resource* vb = nullptr; + ID3D12Resource* ib = nullptr; + D3D12_VERTEX_BUFFER_VIEW vb_view{}; + D3D12_INDEX_BUFFER_VIEW ib_view{}; + uint64_t fingerprint = 0; +}; + +struct RendererState { + ID3D12Device* device = nullptr; + ID3D12RootSignature* root_signature = nullptr; + ID3D12PipelineState* pso = nullptr; + ID3D12PipelineState* pso_nodepth = nullptr; + DXGI_FORMAT rtv_format = DXGI_FORMAT_UNKNOWN; + ID3D12DescriptorHeap* rtv_heap = nullptr; + ID3D12DescriptorHeap* dsv_heap = nullptr; + ID3D12Resource* depth = nullptr; + uint32_t depth_width = 0; + uint32_t depth_height = 0; + ID3D12Resource* rtv_resource = nullptr; + std::unordered_map meshes; + // Buffers replaced by re-decode, kept alive until the GPU has finished the + // submission that last referenced them. + std::vector> retired; + bool failed = false; + bool announced = false; +}; + +RendererState g_r; + +// Face-normal shading uses the camera-relative world position: interpolating +// absolute world coordinates (hundreds of meters) destroys ddx/ddy precision +// and produces per-pixel noise. +const char kShaderSource[] = R"( +cbuffer C : register(b0) { + row_major float4x4 world; + row_major float4x4 mvp; + float4 tint; + float4 cam_pos; +}; +struct VSOut { float4 pos : SV_Position; float3 rpos : TEXCOORD0; }; +VSOut vs_main(float3 p : POSITION) { + VSOut o; + o.pos = mul(float4(p, 1.0), mvp); + o.rpos = mul(float4(p, 1.0), world).xyz - cam_pos.xyz; + return o; +} +float4 ps_main(VSOut i) : SV_Target { + if (tint.a > 0.0) { + return tint; + } + float3 n = normalize(cross(ddx(i.rpos), ddy(i.rpos))); + float l = abs(dot(n, normalize(float3(0.4, 0.8, 0.3)))) * 0.6 + 0.35; + return float4(l, l, l, 1.0); +} +)"; + +float HalfToFloat(uint16_t h) { + const uint32_t sign = (h & 0x8000u) << 16; + uint32_t exp = (h >> 10) & 0x1F; + uint32_t mant = h & 0x3FF; + if (exp == 0) { + if (mant == 0) return std::bit_cast(sign); + // subnormal + while (!(mant & 0x400)) { + mant <<= 1; + --exp; + } + ++exp; + mant &= 0x3FF; + } else if (exp == 31) { + return std::bit_cast(sign | 0x7F800000u | (mant << 13)); + } + return std::bit_cast(sign | ((exp + 112) << 23) | (mant << 13)); +} + +ID3D12Resource* CreateUploadBuffer(ID3D12Device* device, size_t size) { + D3D12_HEAP_PROPERTIES heap{}; + heap.Type = D3D12_HEAP_TYPE_UPLOAD; + D3D12_RESOURCE_DESC desc{}; + desc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; + desc.Width = size; + desc.Height = 1; + desc.DepthOrArraySize = 1; + desc.MipLevels = 1; + desc.SampleDesc.Count = 1; + desc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; + ID3D12Resource* resource = nullptr; + if (FAILED(device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &desc, + D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, + IID_PPV_ARGS(&resource)))) { + return nullptr; + } + return resource; +} + +uint16_t SwapU16(uint16_t v) { return uint16_t((v >> 8) | (v << 8)); } +uint32_t SwapU32(uint32_t v) { +#if defined(_MSC_VER) + return _byteswap_ulong(v); +#else + return __builtin_bswap32(v); +#endif +} + +// Decode guest vertices into float3 positions. +bool DecodeMesh(ID3D12Device* device, uint8_t* base, const DrawItem& item, + MeshBuffers& out) { + const uint32_t num_verts = item.vb_bytes / item.stride; + if (num_verts == 0) return false; + ID3D12Resource* vb = CreateUploadBuffer(device, size_t(num_verts) * 12); + ID3D12Resource* ib = CreateUploadBuffer(device, size_t(item.ib_count) * 2); + if (!vb || !ib) { + if (vb) vb->Release(); + if (ib) ib->Release(); + return false; + } + + const uint8_t* src_vb = base + item.vb_addr; + float* dst = nullptr; + uint32_t garbage = 0; + vb->Map(0, nullptr, reinterpret_cast(&dst)); + for (uint32_t v = 0; v < num_verts; ++v) { + const uint8_t* p = src_vb + size_t(v) * item.stride + item.pos_offset; + float x = 0.0f, y = 0.0f, z = 0.0f; + switch (item.pos_fmt) { + case 57: { // k_32_32_32_FLOAT + x = std::bit_cast(SwapU32(*reinterpret_cast(p))); + y = std::bit_cast(SwapU32(*reinterpret_cast(p + 4))); + z = std::bit_cast(SwapU32(*reinterpret_cast(p + 8))); + break; + } + case 32: { // k_16_16_16_16_FLOAT + x = HalfToFloat(SwapU16(*reinterpret_cast(p))); + y = HalfToFloat(SwapU16(*reinterpret_cast(p + 2))); + z = HalfToFloat(SwapU16(*reinterpret_cast(p + 4))); + break; + } + case 26: { // k_16_16_16_16 snorm character dequant + const auto s16 = [&](int off) { + return int16_t(SwapU16(*reinterpret_cast(p + off))); + }; + constexpr float kScale = 2.0f / 32767.0f; + x = s16(0) * kScale; + y = s16(2) * kScale + 0.8f; + z = s16(4) * kScale; + break; + } + default: + vb->Unmap(0, nullptr); + vb->Release(); + ib->Release(); + return false; + } + if (!(x == x && y == y && z == z) || + x < item.bbox_min[0] - 2.f || x > item.bbox_max[0] + 2.f || + y < item.bbox_min[1] - 2.f || y > item.bbox_max[1] + 2.f || + z < item.bbox_min[2] - 2.f || z > item.bbox_max[2] + 2.f) { + ++garbage; + } + dst[v * 3 + 0] = x; + dst[v * 3 + 1] = y; + dst[v * 3 + 2] = z; + } + vb->Unmap(0, nullptr); + if (garbage != 0) { + REXLOG_WARN( + "native-scene: mesh {:08X} decoded {} of {} verts outside bbox " + "({:.1f},{:.1f},{:.1f})..({:.1f},{:.1f},{:.1f}) fmt {} stride {} vb {:08X}", + item.mesh, garbage, num_verts, item.bbox_min[0], item.bbox_min[1], item.bbox_min[2], + item.bbox_max[0], item.bbox_max[1], item.bbox_max[2], item.pos_fmt, item.stride, + item.vb_addr); + } + + const uint16_t* src_ib = reinterpret_cast(base + item.ib_addr); + uint16_t* dst_ib = nullptr; + ib->Map(0, nullptr, reinterpret_cast(&dst_ib)); + for (uint32_t i = 0; i < item.ib_count; ++i) { + dst_ib[i] = SwapU16(src_ib[i]); + } + ib->Unmap(0, nullptr); + + out.vb = vb; + out.ib = ib; + out.vb_view = {vb->GetGPUVirtualAddress(), num_verts * 12u, 12u}; + out.ib_view = {ib->GetGPUVirtualAddress(), item.ib_count * 2u, DXGI_FORMAT_R16_UINT}; + return true; +} + +bool EnsurePipeline(const NativeGuestOutputRenderContext& context) { + if (g_r.failed) return false; + ID3D12Device* device = context.d3d12.device; + g_r.device = device; + + if (!g_r.root_signature) { + D3D12_ROOT_PARAMETER param{}; + param.ParameterType = D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS; + param.Constants.ShaderRegister = 0; + param.Constants.Num32BitValues = 40; + param.ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + D3D12_ROOT_SIGNATURE_DESC desc{}; + desc.NumParameters = 1; + desc.pParameters = ¶m; + desc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT; + if (!context.d3d12.create_root_signature(context.d3d12.command_processor_user_data, + &desc, &g_r.root_signature)) { + REXLOG_ERROR("native-scene: root signature creation failed"); + g_r.failed = true; + return false; + } + } + + if (!g_r.pso || g_r.rtv_format != context.d3d12.guest_output_format) { + ID3DBlob* vs = nullptr; + ID3DBlob* ps = nullptr; + ID3DBlob* errors = nullptr; + if (FAILED(D3DCompile(kShaderSource, sizeof(kShaderSource) - 1, "native_scene", nullptr, + nullptr, "vs_main", "vs_5_0", 0, 0, &vs, &errors)) || + FAILED(D3DCompile(kShaderSource, sizeof(kShaderSource) - 1, "native_scene", nullptr, + nullptr, "ps_main", "ps_5_0", 0, 0, &ps, &errors))) { + REXLOG_ERROR("native-scene: shader compile failed: {}", + errors ? static_cast(errors->GetBufferPointer()) : "?"); + g_r.failed = true; + return false; + } + D3D12_INPUT_ELEMENT_DESC input{ + "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, + D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0}; + D3D12_GRAPHICS_PIPELINE_STATE_DESC pso{}; + pso.pRootSignature = g_r.root_signature; + pso.VS = {vs->GetBufferPointer(), vs->GetBufferSize()}; + pso.PS = {ps->GetBufferPointer(), ps->GetBufferSize()}; + pso.BlendState.RenderTarget[0].RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL; + pso.SampleMask = UINT_MAX; + pso.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID; + pso.RasterizerState.CullMode = D3D12_CULL_MODE_NONE; + pso.RasterizerState.DepthClipEnable = TRUE; + pso.DepthStencilState.DepthEnable = TRUE; + pso.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL; + pso.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_LESS; + pso.InputLayout = {&input, 1}; + pso.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; + pso.NumRenderTargets = 1; + pso.RTVFormats[0] = context.d3d12.guest_output_format; + pso.DSVFormat = DXGI_FORMAT_D32_FLOAT; + pso.SampleDesc.Count = 1; + const HRESULT hr = device->CreateGraphicsPipelineState(&pso, IID_PPV_ARGS(&g_r.pso)); + pso.DepthStencilState.DepthEnable = FALSE; + pso.DSVFormat = DXGI_FORMAT_UNKNOWN; + const HRESULT hr2 = + device->CreateGraphicsPipelineState(&pso, IID_PPV_ARGS(&g_r.pso_nodepth)); + vs->Release(); + ps->Release(); + if (errors) errors->Release(); + if (FAILED(hr) || FAILED(hr2)) { + REXLOG_ERROR("native-scene: PSO creation failed {:08X}/{:08X}", uint32_t(hr), + uint32_t(hr2)); + g_r.failed = true; + return false; + } + g_r.rtv_format = context.d3d12.guest_output_format; + } + + if (!g_r.rtv_heap) { + D3D12_DESCRIPTOR_HEAP_DESC heap{D3D12_DESCRIPTOR_HEAP_TYPE_RTV, 1, + D3D12_DESCRIPTOR_HEAP_FLAG_NONE, 0}; + if (FAILED(device->CreateDescriptorHeap(&heap, IID_PPV_ARGS(&g_r.rtv_heap)))) { + g_r.failed = true; + return false; + } + heap.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV; + if (FAILED(device->CreateDescriptorHeap(&heap, IID_PPV_ARGS(&g_r.dsv_heap)))) { + g_r.failed = true; + return false; + } + } + + const uint32_t width = context.guest_output_width; + const uint32_t height = context.guest_output_height; + if (!g_r.depth || g_r.depth_width != width || g_r.depth_height != height) { + if (g_r.depth) { + g_r.depth->Release(); + g_r.depth = nullptr; + } + D3D12_HEAP_PROPERTIES heap{}; + heap.Type = D3D12_HEAP_TYPE_DEFAULT; + D3D12_RESOURCE_DESC desc{}; + desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + desc.Width = width; + desc.Height = height; + desc.DepthOrArraySize = 1; + desc.MipLevels = 1; + desc.Format = DXGI_FORMAT_D32_FLOAT; + desc.SampleDesc.Count = 1; + desc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL; + D3D12_CLEAR_VALUE clear{}; + clear.Format = DXGI_FORMAT_D32_FLOAT; + clear.DepthStencil.Depth = 1.0f; + if (FAILED(g_r.device->CreateCommittedResource( + &heap, D3D12_HEAP_FLAG_NONE, &desc, D3D12_RESOURCE_STATE_DEPTH_WRITE, &clear, + IID_PPV_ARGS(&g_r.depth)))) { + g_r.failed = true; + return false; + } + g_r.depth_width = width; + g_r.depth_height = height; + device->CreateDepthStencilView(g_r.depth, nullptr, + g_r.dsv_heap->GetCPUDescriptorHandleForHeapStart()); + } + + if (g_r.rtv_resource != context.d3d12.guest_output_resource) { + device->CreateRenderTargetView(context.d3d12.guest_output_resource, nullptr, + g_r.rtv_heap->GetCPUDescriptorHandleForHeapStart()); + g_r.rtv_resource = context.d3d12.guest_output_resource; + } + return true; +} + +bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_data*/) { + if (!SceneEnabled() || context.backend != NativeGuestOutputBackend::kD3D12) { + return false; + } + uint8_t* base = g_guest_base.load(std::memory_order_relaxed); + if (base == nullptr) { + return false; + } + + FrameScene scene; + { + std::lock_guard lock(g_scene_mutex); + if (g_scene.items.empty()) { + return false; + } + scene = g_scene; + } + + if (!EnsurePipeline(context)) { + return false; + } + + if (!g_r.announced) { + g_r.announced = true; + REXLOG_INFO("native-scene: rendering natively ({} items, {}x{})", scene.items.size(), + context.guest_output_width, context.guest_output_height); + } + + auto* command_processor = context.d3d12.command_processor; + auto& list = command_processor->GetDeferredCommandList(); + + // Free retired buffers whose last-referencing submission has completed. + if (!g_r.retired.empty()) { + const uint64_t completed = command_processor->GetCompletedSubmission(); + std::erase_if(g_r.retired, [completed](const auto& entry) { + if (entry.second < completed) { + entry.first->Release(); + return true; + } + return false; + }); + } + + context.d3d12.push_transition_barrier(context.d3d12.command_processor_user_data, + context.d3d12.guest_output_resource, + context.d3d12.guest_output_initial_state, + D3D12_RESOURCE_STATE_RENDER_TARGET); + context.d3d12.submit_barriers(context.d3d12.command_processor_user_data); + + const int32_t debug_mode_early = REXCVAR_GET(skate3_native_render_scene_debug); + const bool use_depth = debug_mode_early != 4; + const D3D12_CPU_DESCRIPTOR_HANDLE rtv = g_r.rtv_heap->GetCPUDescriptorHandleForHeapStart(); + const D3D12_CPU_DESCRIPTOR_HANDLE dsv = g_r.dsv_heap->GetCPUDescriptorHandleForHeapStart(); + const FLOAT clear_color[4] = {0.25f, 0.35f, 0.55f, 1.0f}; + list.D3DClearRenderTargetView(rtv, clear_color, 0, nullptr); + if (use_depth) { + list.D3DClearDepthStencilView(dsv, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr); + list.D3DOMSetRenderTargets(1, &rtv, FALSE, &dsv); + } else { + list.D3DOMSetRenderTargets(1, &rtv, FALSE, nullptr); + } + + D3D12_VIEWPORT viewport{0.0f, + 0.0f, + float(context.guest_output_width), + float(context.guest_output_height), + 0.0f, + 1.0f}; + list.RSSetViewport(viewport); + D3D12_RECT scissor{0, 0, LONG(context.guest_output_width), + LONG(context.guest_output_height)}; + list.RSSetScissorRect(scissor); + list.D3DSetGraphicsRootSignature(g_r.root_signature); + list.D3DSetPipelineState(use_depth ? g_r.pso : g_r.pso_nodepth); + + const int32_t debug_mode = REXCVAR_GET(skate3_native_render_scene_debug); + uint32_t drawn = 0; + uint32_t item_index = 0; + for (const DrawItem& item : scene.items) { + const uint32_t index = item_index++; + if (debug_mode == 1) { + break; + } + if (debug_mode == 3 && index >= 20) { + break; + } + auto it = g_r.meshes.find(item.mesh); + if (it != g_r.meshes.end() && it->second.fingerprint != item.fingerprint) { + const uint64_t submission = command_processor->GetCurrentSubmission(); + g_r.retired.emplace_back(it->second.vb, submission); + g_r.retired.emplace_back(it->second.ib, submission); + g_r.meshes.erase(it); + it = g_r.meshes.end(); + } + if (it == g_r.meshes.end()) { + MeshBuffers buffers; + if (!DecodeMesh(g_r.device, base, item, buffers)) { + continue; + } + buffers.fingerprint = item.fingerprint; + it = g_r.meshes.emplace(item.mesh, buffers).first; + } + const MeshBuffers& buffers = it->second; + + // constants = world + mvp (world * view_proj, row-vector) + tint + cam. + float constants[40]; + std::memcpy(constants, item.world, sizeof(item.world)); + float* mvp = constants + 16; + for (int r = 0; r < 4; ++r) { + for (int c = 0; c < 4; ++c) { + float sum = 0.0f; + for (int k = 0; k < 4; ++k) { + sum += item.world[r * 4 + k] * scene.view_proj[k * 4 + c]; + } + mvp[r * 4 + c] = sum; + } + } + if (debug_mode >= 2) { + constants[32] = float((index * 37u) % 255u) / 255.0f; + constants[33] = float((index * 73u) % 255u) / 255.0f; + constants[34] = float((index * 151u) % 255u) / 255.0f; + constants[35] = 1.0f; + } else { + constants[32] = constants[33] = constants[34] = constants[35] = 0.0f; + } + constants[36] = scene.cam_pos[0]; + constants[37] = scene.cam_pos[1]; + constants[38] = scene.cam_pos[2]; + constants[39] = 0.0f; + list.D3DSetGraphicsRoot32BitConstants(0, 40, constants, 0); + list.D3DIASetVertexBuffers(0, 1, &buffers.vb_view); + list.D3DIASetIndexBuffer(&buffers.ib_view); + for (const DrawEntry& draw : item.draws) { + if (draw.prim == 4) { + list.D3DIASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + } else if (draw.prim == 6) { + list.D3DIASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); + } else { + continue; + } + list.D3DDrawIndexedInstanced(draw.index_count, 1, draw.start_index, draw.base_vertex, + 0); + ++drawn; + } + } + + context.d3d12.push_transition_barrier(context.d3d12.command_processor_user_data, + context.d3d12.guest_output_resource, + D3D12_RESOURCE_STATE_RENDER_TARGET, + context.d3d12.guest_output_initial_state); + context.d3d12.submit_barriers(context.d3d12.command_processor_user_data); + + const uint64_t frames = g_frames_rendered.fetch_add(1) + 1; + if (frames % 600 == 0) { + REXLOG_INFO("native-scene: frame {} items={} draws={} cached_meshes={}", frames, + scene.items.size(), drawn, g_r.meshes.size()); + } + return true; +} + +} // namespace + +void Install() { + if (!SceneEnabled()) { + return; + } + rex::graphics::SetNativeGuestOutputRenderer(&RenderScene, nullptr); + REXLOG_INFO("native-scene: guest output renderer registered"); +} + +} // namespace skate3::native_scene + +#else // !REX_HAS_D3D12 + +namespace skate3::native_scene { +void Install() {} +} // namespace skate3::native_scene + +#endif // REX_HAS_D3D12 diff --git a/src/skate3_native_scene.h b/src/skate3_native_scene.h new file mode 100644 index 0000000..c2bf69e --- /dev/null +++ b/src/skate3_native_scene.h @@ -0,0 +1,65 @@ +#pragma once + +// Native scene renderer (data-driven): consumes the per-frame MeshContext +// stream collected by skate3_native_render.cpp, walks the verified guest +// structures on the game thread into a +// compact NativeFrameScene, and renders it with our own shaders into the +// guest output texture via the SDK native-guest-output hook, replacing the +// emulated frame when active. + +#include +#include + +namespace skate3::native_scene { + +struct DrawEntry { + uint32_t prim; + uint32_t base_vertex; + uint32_t start_index; + uint32_t index_count; +}; + +struct DrawItem { + uint32_t mesh; // guest mesh object address (resource cache key) + uint32_t vb_addr; // guest address of raw vertex data + uint32_t ib_addr; // guest address of raw index data (u16 big-endian) + uint32_t vb_bytes; + uint32_t ib_count; + uint16_t pos_offset; + uint8_t stride; + uint8_t pos_fmt; // xenos vertex format (26 s16x4, 32 half4, 57 float3) + // Content fingerprint of the guest payloads. Streaming reuses arena + // addresses and pages fill in after the mesh is first submitted, so cached + // GPU copies must be revalidated against this every frame. + uint64_t fingerprint; + float world[16]; // row-vector convention, translation in row 3 + float bbox_min[3]; // mesh-local bounds, for decode validation + float bbox_max[3]; + std::vector draws; +}; + +struct FrameScene { + uint64_t generation = 0; + float view_proj[16] = {}; + float cam_pos[3] = {}; + std::vector items; +}; + +// One frame submission record from the hook layer (see RenderMeshRecord). +struct SubmitRecord { + uint32_t kind; + uint32_t a; + uint32_t b; + uint32_t c; +}; + +bool Enabled(); + +// Called on the guest render thread at frame end with the frame's records. +void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count); + +// Registers the native guest output renderer with the SDK. Safe to call once +// at startup regardless of cvar state. +void Install(); + +} // namespace skate3::native_scene diff --git a/third_party/rexglue-sdk b/third_party/rexglue-sdk index fac7ab2..c4b0dfe 160000 --- a/third_party/rexglue-sdk +++ b/third_party/rexglue-sdk @@ -1 +1 @@ -Subproject commit fac7ab2616aadeb4a8a884d44e0f7064db76435c +Subproject commit c4b0dfe2c9162340d348d1e86d7b0e1aed353101