From 5285ca85fd93622b0ae14cf82d97dae294812069 Mon Sep 17 00:00:00 2001 From: shenlei Date: Mon, 28 Sep 2026 12:54:13 +0900 Subject: [PATCH] native render: transliterate D3D8 fixed-function pipeline (unit A) - Texture coordinates: TEXCOORDINDEX generation (camera-space normal / position / reflection vector, LOCALVIEWER) and D3DTS_TEXTUREn under TEXTURETRANSFORMFLAGS (incl. PROJECTED) now run in native.vert; the RecordingDevice CPU uv baking and the sphere-map / multiplicative-shadow shader hacks are removed. Godot consumer uses Render3DStageTexcoords. - Lighting: 8 D3D lights in camera space, 1/(a0+a1d+a2d^2) without clamp, spot cone theta/2 phi/2 with falloff, diffuse/ambient/emissive material sources, NORMALIZENORMALS, inverse-transpose normal matrix. - Alpha test compares 8-bit alpha against ALPHAREF with ALPHAFUNC. - Back-buffer Clear() is recorded in draw order and executed with vkCmdClearAttachments; render pass clears to 0xff000000. - D3DVIEWPORT8 (incl. MinZ/MaxZ) is dynamic per-draw state. - Missing vertex diffuse is opaque white; alpha blend mirrors SRC/DESTBLEND. - CScreen::ms_clearDepth / ms_diffuseColor initialised to 40250's 1.0f / 0xffffffff (were zero; exposed once clears were executed). - Capture format v8; --screenshot-out FILE.bmp swapchain readback. Co-Authored-By: Claude Opus 5.5 --- audit/history.jsonl | 1 + audit/port-map/EterGrnLib/Material.cpp.json | 3 +- audit/port-map/EterLib/GrpScreen.cpp.json | 12 +- extension/src/platform/EterLib/GrpScreen.cpp | 4 +- .../src/platform/EterLib/RecordingDevice.cpp | 200 ++++++---- .../src/platform/EterLib/RenderCommands3D.h | 25 +- extension/src/python_host_node.cpp | 19 +- native_render/draw_capture.h | 36 +- native_render/main.cpp | 363 ++++++++++++------ native_render/native.frag | 84 ++-- native_render/native.vert | 159 +++++--- 11 files changed, 585 insertions(+), 321 deletions(-) diff --git a/audit/history.jsonl b/audit/history.jsonl index 57ca3dfe..3840ad99 100644 --- a/audit/history.jsonl +++ b/audit/history.jsonl @@ -557,3 +557,4 @@ {"time": "2026-09-24T07:31:08Z", "event": "platform_adapter_fix", "unit": "EterLib/GrpScreen.cpp + MilesLib/SoundManager.cpp", "detail": "Implemented reached 40250 UI box and vertical gradient command bridge; audio bridge now preserves named three-slot music fades, frame-step fade speed and limit, indexed 3D volume, 3D play counts, and SaveVolume/RestoreVolume mute state. Remaining sky/water/shadow/culling/IME adapters stay TODO.", "evidence": ["./script/port_gate.sh macos", "./script/python_game_render_test.sh", "godot --headless --path project -s res://audio_driver_test.gd", "port_map.py check"]} {"date": "2026-09-28T01:31:59+00:00", "unit": "GameLib/MapOutdoorRenderHTP.cpp + GameLib/MapOutdoorCharacterShadow.cpp", "action": "native 40250 render parity: terrain near/mid/far LOD/fog/splat cap, character shadow RTT CPU raster, per-draw depth/sampler states and two-light fixed-function shading", "evidence": "mt_native_render fake-server game 90 frames; 178 3D draws; shadow mem texture 205 nonwhite pixels; matched Windows screenshot pending"} {"date": "2026-09-28T01:35:06+00:00", "unit": "GameLib/MapOutdoorRenderHTP.cpp", "action": "match 40250 lighting state in terrain splat passes: unlit base textures, temporary lit shadow pass, white fog color only for shadow pass", "evidence": "reference MapOutdoorRenderHTP.cpp non-WORLD_EDITOR branch; native fake-server scene rerun pending"} +{"date": "2026-09-28T03:54:13+00:00", "unit": "EterLib/GrpScreen.cpp + native D3D8 fixed-function emulation (RecordingDevice / native_render)", "action": "unit A: D3D8 texcoord generation + D3DTS_TEXTUREn in shader (CPU uv baking removed), 8-light D3D lighting (no attenuation clamp, spot theta/2 phi/2, emissive source, NORMALIZENORMALS, inverse-transpose normals), 8-bit alpha test, clears in draw order, dynamic D3D viewport + MinZ/MaxZ, black back-buffer clear, white default diffuse, alpha blend mirrors colour factors; removed sphere-map and multiplicative-shadow hacks; CScreen ms_clearDepth/ms_diffuseColor initialisers fixed to 40250 values; capture v8; --screenshot-out readback", "evidence": "ctest 35/35 (login_live excluded); mt_native_render fake-server 240/900-frame screenshots vs baseline: game frame mean abs diff 0.55/255, char select model now placed by introselect.py grp.SetViewport(270/w,...); Windows reference frame pending (NEEDS_LIVE)"} diff --git a/audit/port-map/EterGrnLib/Material.cpp.json b/audit/port-map/EterGrnLib/Material.cpp.json index 93ae5dfa..332a7595 100644 --- a/audit/port-map/EterGrnLib/Material.cpp.json +++ b/audit/port-map/EterGrnLib/Material.cpp.json @@ -14,7 +14,8 @@ "status": "PORTED", "impl": [ "extension/src/port/EterGrnLib/Material.cpp:CGrannyMaterial::ApplyRenderState" - ] + ], + "note": "Native renderer no longer special-cases the sphere map: stage 1 CAMERASPACEREFLECTIONVECTOR, D3DTS_TEXTURE1 (ms_matSpecular) and MODULATEALPHA_ADDCOLOR run through the generic D3D8 texcoord/stage emulation. Visual parity NEEDS_LIVE." }, "CGrannyMaterial::RestoreRenderState": { "status": "PORTED", diff --git a/audit/port-map/EterLib/GrpScreen.cpp.json b/audit/port-map/EterLib/GrpScreen.cpp.json index 006bfb5e..a059eabb 100644 --- a/audit/port-map/EterLib/GrpScreen.cpp.json +++ b/audit/port-map/EterLib/GrpScreen.cpp.json @@ -104,10 +104,18 @@ "status": "TODO" }, "CScreen::ClearDepthBuffer": { - "status": "TODO" + "status": "NEEDS_LIVE", + "impl": [ + "extension/src/platform/EterLib/GrpScreen.cpp:CScreen::ClearDepthBuffer" + ], + "note": "Statement-equivalent Clear(ZBUFFER, ms_clearColor, ms_clearDepth, ms_clearStencil). The port had ms_clearDepth/ms_diffuseColor zero-initialized instead of 40250's 1.0f/0xffffffff; fixed. RecordingDevice now records back-buffer clears in draw order and the native renderer executes them (vkCmdClearAttachments over the current viewport); before, the clear was dropped. Native fake-server char select shows the model again with the clear executed; Windows screenshot parity open." }, "CScreen::Clear": { - "status": "TODO" + "status": "NEEDS_LIVE", + "impl": [ + "extension/src/platform/EterLib/GrpScreen.cpp:CScreen::Clear" + ], + "note": "Statement-equivalent Clear(TARGET|ZBUFFER); executed in draw order by the native renderer since the clear-entry change. Windows screenshot parity open." }, "CScreen::IsLostDevice": { "status": "TODO" diff --git a/extension/src/platform/EterLib/GrpScreen.cpp b/extension/src/platform/EterLib/GrpScreen.cpp index d37e219f..869a9290 100644 --- a/extension/src/platform/EterLib/GrpScreen.cpp +++ b/extension/src/platform/EterLib/GrpScreen.cpp @@ -424,12 +424,12 @@ auto CScreen::Identity() -> void STATEMANAGER.SetTransform(D3DTS_WORLD, &ms_matIdentity); } -decltype(CScreen::ms_diffuseColor) CScreen::ms_diffuseColor{}; +decltype(CScreen::ms_diffuseColor) CScreen::ms_diffuseColor = 0xffffffff; decltype(CScreen::ms_clearColor) CScreen::ms_clearColor{}; decltype(CScreen::ms_clearStencil) CScreen::ms_clearStencil{}; -decltype(CScreen::ms_clearDepth) CScreen::ms_clearDepth{}; +decltype(CScreen::ms_clearDepth) CScreen::ms_clearDepth = 1.0f; decltype(CScreen::ms_frustum) CScreen::ms_frustum{}; diff --git a/extension/src/platform/EterLib/RecordingDevice.cpp b/extension/src/platform/EterLib/RecordingDevice.cpp index 4b4626c3..0b55590e 100644 --- a/extension/src/platform/EterLib/RecordingDevice.cpp +++ b/extension/src/platform/EterLib/RecordingDevice.cpp @@ -414,7 +414,16 @@ public: HRESULT Clear(DWORD, const D3DRECT*, DWORD flags, D3DCOLOR color, float depth, DWORD) override { if (m_renderTarget == m_backBuffer) + { + // Kept in draw order: the renderer clears its colour/depth attachments at this point. + Render3DDraw clear; + clear.clear_flags = flags & (D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER | D3DCLEAR_STENCIL); + clear.clear_color = color; + clear.clear_z = depth; + if (clear.clear_flags) + Render3DAdd(std::move(clear)); return S_OK; + } auto* surface = static_cast(m_renderTarget); if (!surface->parent || surface->level != 0 || surface->desc.Format != D3DFMT_R5G6B5) return S_OK; @@ -781,6 +790,8 @@ private: draw.viewport[1] = float(m_viewport.Y); draw.viewport[2] = float(m_viewport.Width); draw.viewport[3] = float(m_viewport.Height); + draw.viewport_z[0] = m_viewport.MinZ; + draw.viewport_z[1] = m_viewport.MaxZ; draw.pretransformed = layout.rhw; draw.lines = type == D3DPT_LINELIST || type == D3DPT_LINESTRIP; @@ -794,15 +805,6 @@ private: if ((size_t(hi) + 1) * stride > vertexBytes) return; const size_t count = size_t(hi - lo) + 1; - const bool texgen_cam_pos0 = layout.uv0 < 0 && m_textures[0] != nullptr && - (m_stageStates[0][D3DTSS_TEXCOORDINDEX] & 0xFFFF0000u) == D3DTSS_TCI_CAMERASPACEPOSITION && - m_stageStates[0][D3DTSS_TEXTURETRANSFORMFLAGS] == D3DTTFF_COUNT2; - const bool texgen_cam_pos1 = layout.uv1 < 0 && m_textures[1] != nullptr && - (m_stageStates[1][D3DTSS_TEXCOORDINDEX] & 0xFFFF0000u) == D3DTSS_TCI_CAMERASPACEPOSITION && - m_stageStates[1][D3DTSS_TEXTURETRANSFORMFLAGS] == D3DTTFF_COUNT2; - const bool tex_xform0 = layout.uv0 >= 0 && m_textures[0] != nullptr && - (m_stageStates[0][D3DTSS_TEXCOORDINDEX] & 0xFFFF0000u) == 0 && - m_stageStates[0][D3DTSS_TEXTURETRANSFORMFLAGS] == D3DTTFF_COUNT2; GpuSkinSubrangeView skin_view; const bool gpu_skinned = source_vertex != nullptr && LookupGpuSkinSubrange(vertices, stride, lo, hi, &skin_view); @@ -839,40 +841,9 @@ private: key = mix(key, type); key = mix(key, stride); key = mix(key, m_fvf); - if (texgen_cam_pos0) - key = mix(key, static_cast(reinterpret_cast(m_textures[0]))); - if (texgen_cam_pos1) - key = mix(key, static_cast(reinterpret_cast(m_textures[1]))); draw.geometry_key = (key & 0x7FFFFFFFFFFFFFFFull) | 1ull; std::uint64_t revision = mix(14695981039346656037ull, source_vertex->revision); revision = mix(revision, source_index ? source_index->revision : 0); - if (tex_xform0) - { - const float* m = m_transforms[D3DTS_TEXTURE0]; - for (int mi : { 0, 1, 4, 5, 8, 9, 12, 13 }) - { - std::uint32_t bits = 0; - std::memcpy(&bits, &m[mi], sizeof(bits)); - revision = mix(revision, bits); - } - } - // Camera-space texture coordinates are baked into the captured geometry. - // The terrain splat and character-shadow matrices can change while the - // vertex/index buffers remain unchanged, so they are part of its revision. - for (int stage = 0; stage < 2; ++stage) - { - if ((stage == 0 && !texgen_cam_pos0) || (stage == 1 && !texgen_cam_pos1)) - continue; - float worldView[16], worldTexture[16]; - multiply(m_transforms[256], m_transforms[D3DTS_VIEW], worldView); - multiply(worldView, m_transforms[stage == 0 ? D3DTS_TEXTURE0 : D3DTS_TEXTURE1], worldTexture); - for (int mi : { 0, 1, 4, 5, 8, 9, 12, 13 }) - { - std::uint32_t bits = 0; - std::memcpy(&bits, &worldTexture[mi], sizeof(bits)); - revision = mix(revision, bits); - } - } draw.geometry_revision = revision & 0x7FFFFFFFFFFFFFFFull; } } @@ -912,49 +883,6 @@ private: if (layout.uv1 >= 0) std::memcpy(&draw.uv1[i * 2], v + layout.uv1, 8); if (layout.diffuse >= 0) std::memcpy(&draw.diffuse[i], v + layout.diffuse, 4); } - if (texgen_cam_pos0) - { - float worldView[16], worldTex0[16]; - multiply(m_transforms[256], m_transforms[D3DTS_VIEW], worldView); - multiply(worldView, m_transforms[D3DTS_TEXTURE0], worldTex0); - draw.uv0.resize(count * 2); - const bool clamp0_u = m_stageStates[0][D3DTSS_ADDRESSU] == D3DTADDRESS_CLAMP; - const bool clamp0_v = m_stageStates[0][D3DTSS_ADDRESSV] == D3DTADDRESS_CLAMP; - for (size_t i = 0; i < count; ++i) - { - const float pos[4] = { draw.positions[i * 3 + 0], draw.positions[i * 3 + 1], draw.positions[i * 3 + 2], 1.0f }; - float tc[4]; - transform_point(pos, worldTex0, tc); - draw.uv0[i * 2 + 0] = clamp0_u ? std::clamp(tc[0], 0.5f / 256.0f, 255.5f / 256.0f) : tc[0]; - draw.uv0[i * 2 + 1] = clamp0_v ? std::clamp(tc[1], 0.5f / 256.0f, 255.5f / 256.0f) : tc[1]; - } - } - else if (tex_xform0) - { - const float* m = m_transforms[D3DTS_TEXTURE0]; - for (size_t i = 0; i < count; ++i) - { - const float u = draw.uv0[i * 2 + 0]; - const float v = draw.uv0[i * 2 + 1]; - draw.uv0[i * 2 + 0] = u * m[0] + v * m[4] + m[8] + m[12]; - draw.uv0[i * 2 + 1] = u * m[1] + v * m[5] + m[9] + m[13]; - } - } - if (texgen_cam_pos1) - { - float worldView[16], worldTex1[16]; - multiply(m_transforms[256], m_transforms[D3DTS_VIEW], worldView); - multiply(worldView, m_transforms[D3DTS_TEXTURE1], worldTex1); - draw.uv1.resize(count * 2); - for (size_t i = 0; i < count; ++i) - { - const float pos[4] = { draw.positions[i * 3 + 0], draw.positions[i * 3 + 1], draw.positions[i * 3 + 2], 1.0f }; - float tc[4]; - transform_point(pos, worldTex1, tc); - draw.uv1[i * 2 + 0] = tc[0]; - draw.uv1[i * 2 + 1] = tc[1]; - } - } if (gpu_skinned) { const std::size_t rel_lo = std::size_t(lo - skin_view.mesh_base_vertex); @@ -1033,7 +961,10 @@ private: draw.ambient = m_renderStates[D3DRS_AMBIENT]; draw.diffuse_material_source = m_renderStates[D3DRS_DIFFUSEMATERIALSOURCE]; draw.ambient_material_source = m_renderStates[D3DRS_AMBIENTMATERIALSOURCE]; + draw.emissive_material_source = m_renderStates[D3DRS_EMISSIVEMATERIALSOURCE]; draw.color_vertex = m_renderStates[D3DRS_COLORVERTEX]; + draw.normalize_normals = m_renderStates[D3DRS_NORMALIZENORMALS]; + draw.local_viewer = m_renderStates[D3DRS_LOCALVIEWER]; for (int stage = 0; stage < 2; ++stage) { draw.color_op[stage] = m_stageStates[stage][D3DTSS_COLOROP]; @@ -1047,6 +978,10 @@ private: draw.min_filter[stage] = m_stageStates[stage][D3DTSS_MINFILTER]; draw.mag_filter[stage] = m_stageStates[stage][D3DTSS_MAGFILTER]; draw.mip_filter[stage] = m_stageStates[stage][D3DTSS_MIPFILTER]; + draw.border_color[stage] = m_stageStates[stage][D3DTSS_BORDERCOLOR]; + draw.texcoord_index[stage] = m_stageStates[stage][D3DTSS_TEXCOORDINDEX]; + draw.texture_transform_flags[stage] = m_stageStates[stage][D3DTSS_TEXTURETRANSFORMFLAGS]; + std::memcpy(draw.texture_matrix[stage], m_transforms[D3DTS_TEXTURE0 + stage], sizeof(draw.texture_matrix[stage])); } copy_color(draw.material_diffuse, m_material.Diffuse); copy_color(draw.material_ambient, m_material.Ambient); @@ -1060,7 +995,7 @@ private: copy_color(draw.light0_diffuse, m_lights[0].Diffuse); copy_color(draw.light0_ambient, m_lights[0].Ambient); } - for (int i = 0; i < 2; ++i) + for (int i = 0; i < 8; ++i) { if (!m_lightEnabled[i]) continue; const D3DLIGHT8& source = m_lights[i]; @@ -1217,3 +1152,100 @@ void Render3DAdd(Render3DDraw draw) } const std::vector& Render3DDraws() { return g_draws; } + +// D3D8 fixed-function texture coordinate processing for one stage (the native renderer's +// native.vert applies the same rules on the GPU): +// - D3DTSS_TEXCOORDINDEX low word picks the vertex set, a 2D set enters the transform as (u, v, 1, 0) +// (so a D3DTS_TEXTUREn translation lives in _31/_32, as CSkyBox::RenderCloud writes it); +// - D3DTSS_TCI_CAMERASPACENORMAL/POSITION/REFLECTIONVECTOR generate (x, y, z, 1) in camera space; +// - D3DTSS_TEXTURETRANSFORMFLAGS != DISABLE multiplies by D3DTS_TEXTUREn (row vector), and +// D3DTTFF_PROJECTED divides by the last counted element. +void Render3DStageTexcoords(const Render3DDraw& draw, int stage, std::vector& out) +{ + const size_t count = draw.positions.size() / 3; + out.assign(count * 2, 0.0f); + if (stage < 0 || stage > 1) + return; + const std::uint32_t tci = draw.texcoord_index[stage]; + const std::uint32_t gen = tci & 0xFFFF0000u; + const std::uint32_t set = tci & 0xFFFFu; + const std::vector* uv = set == 0 ? &draw.uv0 : (set == 1 ? &draw.uv1 : nullptr); + const std::uint32_t flags = draw.texture_transform_flags[stage]; + const std::uint32_t elements = flags & 0xFFu; + const bool projected = (flags & D3DTTFF_PROJECTED) != 0; + float worldView[16] = {}; + for (int row = 0; row < 4; ++row) + for (int col = 0; col < 4; ++col) + for (int k = 0; k < 4; ++k) + worldView[row * 4 + col] += draw.world[row * 4 + k] * draw.view[k * 4 + col]; + // Normals go through the inverse transpose of world * view (cofactors / determinant). + const float* w = worldView; + float normalMatrix[9] = { + w[5] * w[10] - w[6] * w[9], -(w[4] * w[10] - w[6] * w[8]), w[4] * w[9] - w[5] * w[8], + -(w[1] * w[10] - w[2] * w[9]), w[0] * w[10] - w[2] * w[8], -(w[0] * w[9] - w[1] * w[8]), + w[1] * w[6] - w[2] * w[5], -(w[0] * w[6] - w[2] * w[4]), w[0] * w[5] - w[1] * w[4] }; + const float det = w[0] * normalMatrix[0] + w[1] * normalMatrix[1] + w[2] * normalMatrix[2]; + if (det != 0.0f) + for (float& c : normalMatrix) c /= det; + const float* m = draw.texture_matrix[stage]; + for (size_t i = 0; i < count; ++i) + { + float in[4] = { 0, 0, 1, 0 }; + if (gen == 0 || draw.pretransformed) + { + if (uv && uv->size() >= (i + 1) * 2) + { + in[0] = (*uv)[i * 2 + 0]; + in[1] = (*uv)[i * 2 + 1]; + } + } + else + { + const float* p = &draw.positions[i * 3]; + float eye[3], n[3] = { 0, 0, 0 }; + for (int c = 0; c < 3; ++c) + eye[c] = p[0] * worldView[c] + p[1] * worldView[4 + c] + p[2] * worldView[8 + c] + worldView[12 + c]; + if (draw.normals.size() >= (i + 1) * 3) + { + const float* src = &draw.normals[i * 3]; + for (int c = 0; c < 3; ++c) + n[c] = src[0] * normalMatrix[c] + src[1] * normalMatrix[3 + c] + src[2] * normalMatrix[6 + c]; + if (draw.normalize_normals) + { + const float len = std::sqrt(n[0] * n[0] + n[1] * n[1] + n[2] * n[2]); + if (len > 0.0f) + for (float& c : n) c /= len; + } + } + float v[3] = { 0, 0, 0 }; + if (gen == D3DTSS_TCI_CAMERASPACENORMAL) + std::memcpy(v, n, sizeof(v)); + else if (gen == D3DTSS_TCI_CAMERASPACEPOSITION) + std::memcpy(v, eye, sizeof(v)); + else if (gen == D3DTSS_TCI_CAMERASPACEREFLECTIONVECTOR) + { + float e[3] = { 0, 0, -1 }; + if (draw.local_viewer) + { + const float len = std::sqrt(eye[0] * eye[0] + eye[1] * eye[1] + eye[2] * eye[2]); + for (int c = 0; c < 3; ++c) e[c] = len > 0.0f ? -eye[c] / len : 0.0f; + } + const float en = e[0] * n[0] + e[1] * n[1] + e[2] * n[2]; + for (int c = 0; c < 3; ++c) v[c] = 2.0f * en * n[c] - e[c]; + } + in[0] = v[0]; in[1] = v[1]; in[2] = v[2]; in[3] = 1.0f; + } + float r[4] = { in[0], in[1], in[2], in[3] }; + // Pre-transformed (XYZRHW) vertices pass their coordinates through untransformed. + if (elements != D3DTTFF_DISABLE && !draw.pretransformed) + for (int c = 0; c < 4; ++c) + r[c] = in[0] * m[c] + in[1] * m[4 + c] + in[2] * m[8 + c] + in[3] * m[12 + c]; + if (projected && !draw.pretransformed && elements >= 2 && elements <= 4 && r[elements - 1] != 0.0f) + { + r[0] /= r[elements - 1]; + r[1] /= r[elements - 1]; + } + out[i * 2 + 0] = r[0]; + out[i * 2 + 1] = r[1]; + } +} diff --git a/extension/src/platform/EterLib/RenderCommands3D.h b/extension/src/platform/EterLib/RenderCommands3D.h index 859728dc..57b73a59 100644 --- a/extension/src/platform/EterLib/RenderCommands3D.h +++ b/extension/src/platform/EterLib/RenderCommands3D.h @@ -18,6 +18,13 @@ struct Render3DDraw { float view[16]; float proj[16]; float viewport[4] = {}; // x, y, width, height + float viewport_z[2] = {0, 1}; // MinZ, MaxZ + + // IDirect3DDevice8::Clear on the back buffer, kept in draw order. A clear entry has no geometry; + // clear_flags holds D3DCLEAR_TARGET / D3DCLEAR_ZBUFFER / D3DCLEAR_STENCIL. + std::uint32_t clear_flags = 0; + std::uint32_t clear_color = 0; // 0xAARRGGBB + float clear_z = 1; // Stage 0/1 textures, named like UIRenderTextureName: the pack path of a file texture or // "mem:@"; empty when the stage has no texture. @@ -51,6 +58,14 @@ struct Render3DDraw { std::uint32_t alpha_op[2] = {}, alpha_arg1[2] = {}, alpha_arg2[2] = {}; std::uint32_t address_u[2] = {}, address_v[2] = {}; std::uint32_t min_filter[2] = {}, mag_filter[2] = {}, mip_filter[2] = {}; + std::uint32_t border_color[2] = {}; + // D3DTSS_TEXCOORDINDEX (set index | D3DTSS_TCI_* generation mode), D3DTSS_TEXTURETRANSFORMFLAGS and + // D3DTS_TEXTURE0/1 per stage. The renderer generates and transforms texture coordinates from + // these the way the D3D8 fixed-function vertex pipeline does; uv0/uv1 stay the raw vertex sets. + std::uint32_t texcoord_index[2] = {0, 1}; + std::uint32_t texture_transform_flags[2] = {}; + float texture_matrix[2][16] = {{1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1}, + {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1}}; // D3DMATERIAL8 diffuse/ambient/emissive (r, g, b, a) and light 0 when enabled. float material_diffuse[4] = {1, 1, 1, 1}; float material_ambient[4] = {}; @@ -69,12 +84,20 @@ struct Render3DDraw { float attenuation[3] = {}; float range = 0; float theta = 0, phi = 0, falloff = 0; - } lights[2]; + } lights[8]; std::uint32_t diffuse_material_source = 0; std::uint32_t ambient_material_source = 0; + std::uint32_t emissive_material_source = 0; std::uint32_t color_vertex = 0; + std::uint32_t normalize_normals = 0; // D3DRS_NORMALIZENORMALS + std::uint32_t local_viewer = 1; // D3DRS_LOCALVIEWER }; +// The texture coordinates stage 0/1 samples at each vertex, computed on the CPU with the same +// D3D8 fixed-function rules the native renderer's vertex shader applies (TEXCOORDINDEX generation, +// then D3DTS_TEXTUREn under TEXTURETRANSFORMFLAGS). For consumers without that shader (Godot). +void Render3DStageTexcoords(const Render3DDraw& draw, int stage, std::vector& out); + struct GpuSkinSubrangeView { std::uint64_t source_mesh_key = 0; std::uint32_t mesh_base_vertex = 0; diff --git a/extension/src/python_host_node.cpp b/extension/src/python_host_node.cpp index 95b15817..ee556895 100644 --- a/extension/src/python_host_node.cpp +++ b/extension/src/python_host_node.cpp @@ -403,6 +403,8 @@ Array Metin2PythonHost::render3d_draws() { out.resize(static_cast(draws.size())); int64_t draw_index = 0; for (const Render3DDraw &draw : draws) { + if (draw.clear_flags) + continue; // back-buffer clears are consumed by the native renderer only Dictionary item; item["geometry_key"] = static_cast(draw.geometry_key); item["geometry_revision"] = static_cast(draw.geometry_revision); @@ -414,8 +416,14 @@ Array Metin2PythonHost::render3d_draws() { item["pretransformed"] = draw.pretransformed; item["lines"] = draw.lines; + // Stage-0 coordinates the D3D8 pipeline generates or transforms depend on per-draw matrices, + // so such draws carry them outside the geometry cache. + const bool stage0_texgen = (draw.texcoord_index[0] & 0xFFFF0000u) != 0 || + (draw.texcoord_index[0] & 0xFFFFu) != 0 || draw.texture_transform_flags[0] != 0; + if (stage0_texgen) + item["geometry_key"] = static_cast(0); Dictionary geometry; - if (draw.geometry_key != 0) { + if (draw.geometry_key != 0 && !stage0_texgen) { auto cached = geometry_cache.find(draw.geometry_key); if (cached != geometry_cache.end() && cached->second.revision == draw.geometry_revision) { cached->second.last_used = extraction; @@ -461,7 +469,11 @@ Array Metin2PythonHost::render3d_draws() { } return out; }; - if (!draw.uv0.empty()) + if (stage0_texgen) { + std::vector stage_uv; + Render3DStageTexcoords(draw, 0, stage_uv); + geometry["uv0"] = uvs(stage_uv); + } else if (!draw.uv0.empty()) geometry["uv0"] = uvs(draw.uv0); if (!draw.uv1.empty()) geometry["uv1"] = uvs(draw.uv1); @@ -477,7 +489,7 @@ Array Metin2PythonHost::render3d_draws() { for (int64_t i = 0; i < indices.size(); ++i) indices[i] = static_cast(draw.indices[i]); geometry["indices"] = indices; - if (draw.geometry_key != 0) + if (draw.geometry_key != 0 && !stage0_texgen) geometry_cache[draw.geometry_key] = {draw.geometry_revision, extraction, geometry}; } item.merge(geometry); @@ -530,6 +542,7 @@ Array Metin2PythonHost::render3d_draws() { item["viewport"] = vp; out[draw_index++] = item; } + out.resize(draw_index); if (extraction % 120 == 0) { for (auto it = geometry_cache.begin(); it != geometry_cache.end();) { if (extraction - it->second.last_used > 120) diff --git a/native_render/draw_capture.h b/native_render/draw_capture.h index 92c26779..f913988a 100644 --- a/native_render/draw_capture.h +++ b/native_render/draw_capture.h @@ -20,11 +20,14 @@ // - Version 5: adds per-draw fog color, mode, range and distance/density state. // - Version 6: adds texture address and filtering state for both stages. // - Version 7: adds two fixed-function lights and material color sources. +// - Version 8: eight lights, back-buffer clear entries, viewport MinZ/MaxZ, border color, +// TEXCOORDINDEX / TEXTURETRANSFORMFLAGS / D3DTS_TEXTUREn per stage, emissive +// material source, NORMALIZENORMALS and LOCALVIEWER; UVs are raw vertex sets. // Capture is opt-in and never runs in the normal game path. namespace native_draw_capture { struct Capture { - std::uint32_t version = 7; + std::uint32_t version = 8; std::vector draws; std::unordered_map> textures; std::uint32_t ui_width = 960; @@ -110,7 +113,7 @@ inline void write( std::ofstream file(path, std::ios::binary | std::ios::trunc); if (!file) throw std::runtime_error("cannot create native draw capture: " + path); const std::uint32_t magic = 0x4d544452; // MTDR - const std::uint32_t version = 7; + const std::uint32_t version = 8; const auto count = static_cast(draws.size()); write_scalar(file, magic); write_scalar(file, version); write_scalar(file, count); for (const auto& draw : draws) { @@ -174,6 +177,17 @@ inline void write( write_scalar(file, draw.diffuse_material_source); write_scalar(file, draw.ambient_material_source); write_scalar(file, draw.color_vertex); + file.write(reinterpret_cast(draw.viewport_z), sizeof(draw.viewport_z)); + write_scalar(file, draw.clear_flags); + write_scalar(file, draw.clear_color); + write_scalar(file, draw.clear_z); + file.write(reinterpret_cast(draw.border_color), sizeof(draw.border_color)); + file.write(reinterpret_cast(draw.texcoord_index), sizeof(draw.texcoord_index)); + file.write(reinterpret_cast(draw.texture_transform_flags), sizeof(draw.texture_transform_flags)); + file.write(reinterpret_cast(draw.texture_matrix), sizeof(draw.texture_matrix)); + write_scalar(file, draw.emissive_material_source); + write_scalar(file, draw.normalize_normals); + write_scalar(file, draw.local_viewer); write_vector(file, draw.bone_indices); write_vector(file, draw.bone_weights); write_vector(file, draw.bone_matrices); @@ -223,7 +237,7 @@ inline Capture read_capture(const std::string& path) { if (!file) throw std::runtime_error("cannot open native draw capture: " + path); std::uint32_t magic = 0, version = 0, count = 0; read_scalar(file, magic); read_scalar(file, version); read_scalar(file, count); - if (magic != 0x4d544452 || (version < 1 || version > 7) || count > 10'000) + if (magic != 0x4d544452 || (version < 1 || version > 8) || count > 10'000) throw std::runtime_error("unsupported native draw capture format"); Capture capture; capture.version = version; @@ -296,10 +310,24 @@ inline Capture read_capture(const std::string& path) { file.read(reinterpret_cast(draw.mip_filter), sizeof(draw.mip_filter)); } if (version >= 7) { - file.read(reinterpret_cast(draw.lights), sizeof(draw.lights)); + file.read(reinterpret_cast(draw.lights), + (version >= 8 ? 8 : 2) * sizeof(draw.lights[0])); read_scalar(file, draw.diffuse_material_source); read_scalar(file, draw.ambient_material_source); read_scalar(file, draw.color_vertex); + } + if (version >= 8) { + file.read(reinterpret_cast(draw.viewport_z), sizeof(draw.viewport_z)); + read_scalar(file, draw.clear_flags); + read_scalar(file, draw.clear_color); + read_scalar(file, draw.clear_z); + file.read(reinterpret_cast(draw.border_color), sizeof(draw.border_color)); + file.read(reinterpret_cast(draw.texcoord_index), sizeof(draw.texcoord_index)); + file.read(reinterpret_cast(draw.texture_transform_flags), sizeof(draw.texture_transform_flags)); + file.read(reinterpret_cast(draw.texture_matrix), sizeof(draw.texture_matrix)); + read_scalar(file, draw.emissive_material_source); + read_scalar(file, draw.normalize_normals); + read_scalar(file, draw.local_viewer); } else if (draw.light0) { auto& light = draw.lights[0]; light.type = 3; diff --git a/native_render/main.cpp b/native_render/main.cpp index f772c885..3c33541b 100644 --- a/native_render/main.cpp +++ b/native_render/main.cpp @@ -63,38 +63,40 @@ struct Vertex { struct PushConstants { std::array mvp{}; - std::array tint_color{1.0f, 1.0f, 1.0f, 1.0f}; - std::array ambient_emissive{1.0f, 1.0f, 1.0f, -1.0f}; - std::array light_dir{0.0f, 0.0f, 1.0f, 0.0f}; - std::array light_diffuse{0.0f, 0.0f, 0.0f, 0.0f}; + // x: bone palette base + 1 (0 = not skinned), y: D3DFVF_XYZRHW, z: UI mask batch, w: unused. + std::array params{}; }; -static_assert(sizeof(PushConstants) == 128, "PushConstants must fit within Vulkan's 128-byte minimum guarantee"); +static_assert(sizeof(PushConstants) <= 128, "PushConstants must fit within Vulkan's 128-byte minimum guarantee"); -// std430 payload selected with a dynamic storage-buffer offset for every draw. -// The existing 128-byte push constants cannot hold the D3D8 texture stages and fog state. +// std430 payload selected with a dynamic storage-buffer offset for every draw: the D3D8 +// fixed-function state in effect for the draw (texture stages, texture coordinate processing, +// lighting, material, fog, alpha test). A zero flags.x marks a 2D UI batch. struct alignas(16) FixedFunctionState { - std::array stage0_color{}; // op, arg1, arg2, unused - std::array stage0_alpha{}; // op, arg1, arg2, unused - std::array stage1_color{}; - std::array stage1_alpha{}; + std::array, 2> stage_color{}; // op, arg1, arg2, D3DTSS_TEXCOORDINDEX + std::array, 2> stage_alpha{}; // op, arg1, arg2, D3DTSS_TEXTURETRANSFORMFLAGS std::array fog_color{}; std::array fog_params{}; // start, end, density, range enabled std::array texture_factor{}; std::array world_view{}; std::array flags{}; // fixed function, texture0, texture1, fog mode - std::array world{}; - std::array lighting_flags{}; // enabled, COLORVERTEX, ambient source, diffuse source + std::array normal_matrix{}; // inverse transpose of world * view + std::array lighting_flags{}; // LIGHTING, COLORVERTEX, vertex has diffuse, NORMALIZENORMALS + std::array material_sources{}; // DIFFUSE, AMBIENT, EMISSIVE source, LOCALVIEWER + std::array alpha_test{}; // ALPHATESTENABLE, ALPHAFUNC, ALPHAREF, unused + std::array material_diffuse{}; std::array material_ambient{}; std::array material_emissive{}; std::array global_ambient{}; - std::array, 2> light_position_type{}; - std::array, 2> light_direction_range{}; - std::array, 2> light_diffuse{}; - std::array, 2> light_ambient{}; - std::array, 2> light_attenuation{}; - std::array, 2> light_spot{}; + std::array, 2> texture_matrix{}; + // Lights in camera space. position.w = D3DLIGHTTYPE (0 = disabled). + std::array, 8> light_position_type{}; + std::array, 8> light_direction_range{}; + std::array, 8> light_diffuse{}; + std::array, 8> light_ambient{}; + std::array, 8> light_attenuation{}; // a0, a1, a2, falloff + std::array, 8> light_spot{}; // cos(theta / 2), cos(phi / 2) }; -static_assert(sizeof(FixedFunctionState) == 512); +static_assert(sizeof(FixedFunctionState) == 1264); constexpr std::array kIdentityMatrix = { 1.0f, 0.0f, 0.0f, 0.0f, @@ -116,11 +118,6 @@ std::array draw_mvp(const Render3DDraw& draw) { return multiply(world_view.data(), draw.proj); } -bool draw_is_specular_spheremap(const Render3DDraw& draw) { - // D3DTOP_MODULATEALPHA_ADDCOLOR == 18 on texture stage 1 - return !draw.texture1.empty() && draw.color_op[1] == 18; -} - std::array unpack_argb(std::uint32_t argb) { return { float((argb >> 16) & 255) / 255.0f, @@ -129,93 +126,77 @@ std::array unpack_argb(std::uint32_t argb) { float((argb >> 24) & 255) / 255.0f}; } +// Inverse transpose of the upper 3x3 of a row-vector matrix, the D3D8 normal transform. +std::array normal_matrix(const std::array& m) { + const float a = m[0], b = m[1], c = m[2], d = m[4], e = m[5], f = m[6], g = m[8], h = m[9], i = m[10]; + const float A = e * i - f * h, B = -(d * i - f * g), C = d * h - e * g; + const float D = -(b * i - c * h), E = a * i - c * g, F = -(a * h - b * g); + const float G = b * f - c * e, H = -(a * f - c * d), I = a * e - b * d; + const float det = a * A + b * B + c * C; + std::array out = kIdentityMatrix; + if (det == 0.0f) return out; + const float s = 1.0f / det; + // inverse = adjugate / det; its transpose is the cofactor matrix / det. + out[0] = A * s; out[1] = B * s; out[2] = C * s; + out[4] = D * s; out[5] = E * s; out[6] = F * s; + out[8] = G * s; out[9] = H * s; out[10] = I * s; + return out; +} + PushConstants make_push_constants(const Render3DDraw& draw, float skin_offset_encoded) { PushConstants constants{}; constants.mvp = draw_mvp(draw); - const bool lit = draw.lighting != 0; - for (int c = 0; c < 4; ++c) { - const float base = lit ? draw.material_diffuse[c] : 1.0f; - constants.tint_color[c] = base; - } - const float alpha_cutoff = draw.alpha_test ? (float(draw.alpha_ref) / 255.0f) : -1.0f; - if (lit && draw.light0) { - const auto amb = unpack_argb(draw.ambient); - for (int c = 0; c < 3; ++c) - constants.ambient_emissive[c] = - draw.material_ambient[c] * std::min(amb[c] + draw.light0_ambient[c], 1.0f) + draw.material_emissive[c]; - constants.ambient_emissive[3] = alpha_cutoff; - const float lx = -draw.light0_direction[0]; - const float ly = -draw.light0_direction[1]; - const float lz = -draw.light0_direction[2]; - float mx = draw.world[0] * lx + draw.world[1] * ly + draw.world[2] * lz; - float my = draw.world[4] * lx + draw.world[5] * ly + draw.world[6] * lz; - float mz = draw.world[8] * lx + draw.world[9] * ly + draw.world[10] * lz; - const float len = std::sqrt(mx * mx + my * my + mz * mz); - if (len > 1e-6f) { - mx /= len; my /= len; mz /= len; - } else { - mx = 0.0f; my = 0.0f; mz = 1.0f; - } - constants.light_dir = {mx, my, mz, 1.0f}; - constants.light_diffuse = { - draw.light0_diffuse[0], draw.light0_diffuse[1], draw.light0_diffuse[2], skin_offset_encoded}; - } else if (lit) { - const auto amb = unpack_argb(draw.ambient); - for (int c = 0; c < 3; ++c) - constants.ambient_emissive[c] = - draw.material_ambient[c] * amb[c] + draw.material_emissive[c]; - constants.ambient_emissive[3] = alpha_cutoff; - constants.light_dir = {0.0f, 0.0f, 1.0f, 1.0f}; - constants.light_diffuse[3] = skin_offset_encoded; - } else { - constants.ambient_emissive = {1.0f, 1.0f, 1.0f, alpha_cutoff}; - constants.light_diffuse[3] = skin_offset_encoded; - } - if (draw_is_specular_spheremap(draw)) { - constants.light_dir[3] = 2.0f; - } else if (!draw.texture1.empty() && (draw.color_op[1] > 1 || draw.alpha_op[1] > 1)) { - constants.light_dir[3] = lit ? -0.6f : -1.0f; - } - if (draw.pretransformed) - constants.light_diffuse[3] = -1.0f; + constants.params = {skin_offset_encoded, draw.pretransformed ? 1.0f : 0.0f, 0.0f, 0.0f}; return constants; } FixedFunctionState make_fixed_function_state(const Render3DDraw& draw) { FixedFunctionState state{}; for (int stage = 0; stage < 2; ++stage) { - auto& color = stage ? state.stage1_color : state.stage0_color; - auto& alpha = stage ? state.stage1_alpha : state.stage0_alpha; - color = {draw.color_op[stage], draw.color_arg1[stage], draw.color_arg2[stage], 0}; - alpha = {draw.alpha_op[stage], draw.alpha_arg1[stage], draw.alpha_arg2[stage], - stage == 0 ? draw.alpha_func : draw.alpha_test}; + state.stage_color[stage] = {draw.color_op[stage], draw.color_arg1[stage], draw.color_arg2[stage], + draw.texcoord_index[stage]}; + state.stage_alpha[stage] = {draw.alpha_op[stage], draw.alpha_arg1[stage], draw.alpha_arg2[stage], + draw.texture_transform_flags[stage]}; + std::copy(draw.texture_matrix[stage], draw.texture_matrix[stage] + 16, state.texture_matrix[stage].begin()); } state.texture_factor = unpack_argb(draw.texture_factor); state.world_view = multiply(draw.world, draw.view); - const auto fog = unpack_argb(draw.fog_color); - state.fog_color = fog; + state.normal_matrix = normal_matrix(state.world_view); + state.fog_color = unpack_argb(draw.fog_color); state.fog_params = {draw.fog_start, draw.fog_end, draw.fog_density, draw.fog_range_enable ? 1.0f : 0.0f}; const std::uint32_t fog_mode = draw.fog_enable ? (draw.fog_table_mode ? draw.fog_table_mode : draw.fog_vertex_mode) : 0; - const bool multiplicative_shadow = draw.alpha_blend && draw.src_blend == 1 && - draw.dest_blend == 3 && !draw.texture0.empty() && draw.color_op[0] == 4; - state.flags = {multiplicative_shadow ? 2u : 1u, !draw.texture0.empty() ? 1u : 0u, - !draw.texture1.empty() ? 1u : 0u, fog_mode}; - std::copy(draw.world, draw.world + 16, state.world.begin()); - state.lighting_flags = {draw.lighting, draw.color_vertex, - draw.ambient_material_source, draw.diffuse_material_source}; + state.flags = {1u, !draw.texture0.empty() ? 1u : 0u, !draw.texture1.empty() ? 1u : 0u, fog_mode}; + state.lighting_flags = {draw.lighting, draw.color_vertex, draw.diffuse.empty() ? 0u : 1u, + draw.normalize_normals}; + state.material_sources = {draw.diffuse_material_source, draw.ambient_material_source, + draw.emissive_material_source, draw.local_viewer}; + state.alpha_test = {draw.alpha_test, draw.alpha_func, draw.alpha_ref, 0}; + std::copy(draw.material_diffuse, draw.material_diffuse + 4, state.material_diffuse.begin()); std::copy(draw.material_ambient, draw.material_ambient + 4, state.material_ambient.begin()); std::copy(draw.material_emissive, draw.material_emissive + 4, state.material_emissive.begin()); state.global_ambient = unpack_argb(draw.ambient); - for (int i = 0; i < 2; ++i) { + const float* v = draw.view; + for (int i = 0; i < 8; ++i) { const auto& light = draw.lights[i]; - state.light_position_type[i] = {light.position[0], light.position[1], light.position[2], float(light.type)}; - state.light_direction_range[i] = {light.direction[0], light.direction[1], light.direction[2], light.range}; + const float* p = light.position; + const float* d = light.direction; + std::array position{}, direction{}; + for (int c = 0; c < 3; ++c) { + position[c] = p[0] * v[c] + p[1] * v[4 + c] + p[2] * v[8 + c] + v[12 + c]; + direction[c] = d[0] * v[c] + d[1] * v[4 + c] + d[2] * v[8 + c]; + } + const float length = std::sqrt(direction[0] * direction[0] + direction[1] * direction[1] + + direction[2] * direction[2]); + if (length > 0.0f) + for (float& c : direction) c /= length; + state.light_position_type[i] = {position[0], position[1], position[2], float(light.type)}; + state.light_direction_range[i] = {direction[0], direction[1], direction[2], light.range}; std::copy(light.diffuse, light.diffuse + 4, state.light_diffuse[i].begin()); std::copy(light.ambient, light.ambient + 4, state.light_ambient[i].begin()); state.light_attenuation[i] = {light.attenuation[0], light.attenuation[1], light.attenuation[2], light.falloff}; - state.light_spot[i] = {light.theta, light.phi, 0, 0}; + state.light_spot[i] = {std::cos(light.theta * 0.5f), std::cos(light.phi * 0.5f), 0, 0}; } return state; } @@ -784,6 +765,12 @@ class VulkanWindow { PushConstants constants{}; FixedFunctionState fixed_state{}; std::uint32_t state_offset = 0; + VkViewport viewport{}; + // IDirect3DDevice8::Clear recorded in draw order; geometry is null for these. + std::uint32_t clear_flags = 0; + VkClearValue clear_color{}; + VkClearValue clear_depth{}; + VkRect2D clear_rect{}; }; struct UiBatch { std::uint32_t first_index = 0; @@ -966,6 +953,30 @@ public: SDL_Quit(); } + // 32-bit bottom-up BMP from a B8G8R8A8 / R8G8B8A8 swapchain readback. + void write_bmp(const std::string& path, const std::uint8_t* pixels) const { + const std::uint32_t w = extent_.width, h = extent_.height; + const bool rgba = swapchain_format_ == VK_FORMAT_R8G8B8A8_UNORM || swapchain_format_ == VK_FORMAT_R8G8B8A8_SRGB; + std::vector file(54 + std::size_t(w) * h * 4); + auto put32 = [&](std::size_t at, std::uint32_t value) { std::memcpy(&file[at], &value, 4); }; + file[0] = 'B'; file[1] = 'M'; + put32(2, std::uint32_t(file.size())); put32(10, 54); put32(14, 40); + put32(18, w); put32(22, h); put32(26, 1u | (32u << 16)); put32(34, w * h * 4); + for (std::uint32_t y = 0; y < h; ++y) + for (std::uint32_t x = 0; x < w; ++x) { + const std::uint8_t* src = pixels + (std::size_t(y) * w + x) * 4; + std::uint8_t* dst = &file[54 + (std::size_t(h - 1 - y) * w + x) * 4]; + dst[0] = rgba ? src[2] : src[0]; dst[1] = src[1]; dst[2] = rgba ? src[0] : src[2]; dst[3] = 255; + } + std::ofstream out(path, std::ios::binary); + out.write(reinterpret_cast(file.data()), std::streamsize(file.size())); + } + + void request_screenshot(std::string path, std::uint64_t frame) { + screenshot_path_ = std::move(path); + screenshot_frame_ = frame; + } + std::uint32_t width() const { return extent_.width; } std::uint32_t height() const { return extent_.height; } std::uint32_t logical_width() const { @@ -979,6 +990,19 @@ public: return h > 0 ? static_cast(h) : extent_.height; } + VkViewport full_viewport() const { + return {0, 0, float(extent_.width), float(extent_.height), 0, 1}; + } + + // D3DVIEWPORT8 (in the game's logical screen pixels) scaled to the swapchain extent. + VkViewport draw_viewport(const Render3DDraw& draw) const { + if (draw.viewport[2] <= 0 || draw.viewport[3] <= 0) return full_viewport(); + const float sx = float(extent_.width) / float(logical_width()); + const float sy = float(extent_.height) / float(logical_height()); + return {draw.viewport[0] * sx, draw.viewport[1] * sy, draw.viewport[2] * sx, draw.viewport[3] * sy, + draw.viewport_z[0], draw.viewport_z[1]}; + } + TouchController touch_controller; static int sdl_scancode_to_dik(SDL_Scancode sc) { @@ -1327,6 +1351,23 @@ public: ensure_bone_capacity(required_bones); for (const auto& draw : draws) { + if (draw.clear_flags) { + PreparedDraw clear{}; + clear.clear_flags = draw.clear_flags; + const auto color = unpack_argb(draw.clear_color); + clear.clear_color.color = {{color[0], color[1], color[2], color[3]}}; + clear.clear_depth.depthStencil = {draw.clear_z, 0}; + // D3D8 Clear with no rectangles clears the current viewport. + const auto viewport = draw_viewport(draw); + const float x0 = std::clamp(viewport.x, 0.0f, float(extent_.width)); + const float y0 = std::clamp(viewport.y, 0.0f, float(extent_.height)); + const float x1 = std::clamp(viewport.x + viewport.width, 0.0f, float(extent_.width)); + const float y1 = std::clamp(viewport.y + viewport.height, 0.0f, float(extent_.height)); + clear.clear_rect = {{std::int32_t(x0), std::int32_t(y0)}, + {std::uint32_t(x1 - x0), std::uint32_t(y1 - y0)}}; + prepared_draws.push_back(clear); + continue; + } if (draw.positions.empty() || draw.indices.empty()) continue; const auto signature = draw.geometry_key ? draw.geometry_revision : geometry_hash(draw); const GeometryId id{draw.geometry_key, signature}; @@ -1354,7 +1395,6 @@ public: } else throw std::runtime_error("bone palette capacity exceeded"); } - const bool is_specular = draw_is_specular_spheremap(draw); const bool is_alpha = draw.alpha_blend != 0; std::uint8_t cull = 0; // D3D8 cull mode names describe the winding to discard. With the @@ -1373,7 +1413,8 @@ public: } const auto pipeline = get_pipeline(cull, depth, blend, draw.lines, static_cast(draw.z_func)); - const bool bind_tex1 = is_specular || (!draw.texture1.empty() && (draw.color_op[1] > 1 || draw.alpha_op[1] > 1)); + // D3DTOP_DISABLE (1) on stage 0 or 1 ends the cascade before stage 1 samples. + const bool bind_tex1 = !draw.texture1.empty() && draw.color_op[0] > 1 && draw.color_op[1] > 1; const auto descriptor = get_texture_descriptor( draw.texture0, bind_tex1 ? draw.texture1 : "", capture_textures, draw); @@ -1386,12 +1427,15 @@ public: 0, 0, 1, 0, -1, -1, 0, 1}; } - prepared_draws.push_back({ - &geometry, - pipeline, - descriptor, - constants, - make_fixed_function_state(draw)}); + PreparedDraw prepared_draw{}; + prepared_draw.geometry = &geometry; + prepared_draw.pipeline = pipeline; + prepared_draw.descriptor = descriptor; + prepared_draw.constants = constants; + prepared_draw.fixed_state = make_fixed_function_state(draw); + // XYZRHW vertices are already in screen space and bypass the viewport transform. + prepared_draw.viewport = draw.pretransformed ? full_viewport() : draw_viewport(draw); + prepared_draws.push_back(prepared_draw); vertex_count += geometry.vertex_count; index_count += geometry.index_count; } @@ -1424,6 +1468,7 @@ public: ensure_state_capacity(prepared_draws.size() + ui_batches.size()); std::size_t state_index = 0; for (auto& draw : prepared_draws) { + if (!draw.geometry) continue; draw.state_offset = static_cast(state_index * state_stride_); std::memcpy(state_mapped_ + draw.state_offset, &draw.fixed_state, sizeof(FixedFunctionState)); ++state_index; @@ -1444,7 +1489,9 @@ public: vkCmdWriteTimestamp(command_, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, query_pool_, 0); VkClearValue clears[2]{}; - clears[0].color = {{0.035f, 0.05f, 0.075f, 1.0f}}; + // 40250 clears the back buffer to 0xff000000 once at device creation and afterwards + // only clears depth each frame (CPythonApplication::Process -> ClearDepthBuffer). + clears[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}}; clears[1].depthStencil = {1.0f, 0}; VkRenderPassBeginInfo pass_begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; pass_begin.renderPass = pass_; pass_begin.framebuffer = framebuffers_.at(image_index); @@ -1454,8 +1501,16 @@ public: VkPipeline current_pipeline = VK_NULL_HANDLE; VkDescriptorSet current_descriptor = VK_NULL_HANDLE; + VkViewport current_viewport{-1, -1, -1, -1, -1, -1}; + auto set_viewport = [&](const VkViewport& viewport) { + if (std::memcmp(&viewport, ¤t_viewport, sizeof(viewport)) == 0) return; + vkCmdSetViewport(command_, 0, 1, &viewport); + current_viewport = viewport; + }; + auto record_ui_pass = [&](bool behind_3d) { bool ui_bound = false; + set_viewport(full_viewport()); for (const auto& batch : ui_batches) { if (batch.behind_3d != behind_3d) continue; if (!ui_bound) { @@ -1489,6 +1544,26 @@ public: record_ui_pass(true); for (const auto& prepared_draw : prepared_draws) { + if (!prepared_draw.geometry) { + VkClearAttachment attachments[2]{}; + std::uint32_t attachment_count = 0; + if (prepared_draw.clear_flags & 1u) { // D3DCLEAR_TARGET + attachments[attachment_count].aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + attachments[attachment_count].colorAttachment = 0; + attachments[attachment_count].clearValue = prepared_draw.clear_color; + ++attachment_count; + } + if (prepared_draw.clear_flags & 2u) { // D3DCLEAR_ZBUFFER + attachments[attachment_count].aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + attachments[attachment_count].clearValue = prepared_draw.clear_depth; + ++attachment_count; + } + const VkClearRect rect{prepared_draw.clear_rect, 0, 1}; + if (attachment_count && rect.rect.extent.width && rect.rect.extent.height) + vkCmdClearAttachments(command_, attachment_count, attachments, 1, &rect); + continue; + } + set_viewport(prepared_draw.viewport); if (prepared_draw.pipeline != current_pipeline) { vkCmdBindPipeline(command_, VK_PIPELINE_BIND_POINT_GRAPHICS, prepared_draw.pipeline); current_pipeline = prepared_draw.pipeline; @@ -1517,6 +1592,43 @@ public: record_ui_pass(false); vkCmdEndRenderPass(command_); + VkBuffer readback = VK_NULL_HANDLE; + VkDeviceMemory readback_memory = VK_NULL_HANDLE; + const bool screenshot = !screenshot_path_.empty() && frame_number_ == screenshot_frame_; + if (screenshot) { + const VkDeviceSize bytes = VkDeviceSize(extent_.width) * extent_.height * 4; + VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + info.size = bytes; info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; + check(vkCreateBuffer(device_, &info, nullptr, &readback), "vkCreateBuffer readback"); + VkMemoryRequirements requirements{}; + vkGetBufferMemoryRequirements(device_, readback, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = find_memory_type(requirements.memoryTypeBits, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + check(vkAllocateMemory(device_, &allocation, nullptr, &readback_memory), "vkAllocateMemory readback"); + check(vkBindBufferMemory(device_, readback, readback_memory, 0), "vkBindBufferMemory readback"); + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = swapchain_images_.at(image_index); + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + vkCmdPipelineBarrier(command_, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + VkBufferImageCopy region{}; + region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.imageExtent = {extent_.width, extent_.height, 1}; + vkCmdCopyImageToBuffer(command_, barrier.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback, 1, ®ion); + barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier.dstAccessMask = 0; + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + vkCmdPipelineBarrier(command_, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + } vkCmdWriteTimestamp(command_, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, query_pool_, 1); check(vkEndCommandBuffer(command_), "vkEndCommandBuffer"); has_pending_query_ = true; @@ -1527,6 +1639,15 @@ public: submit.commandBufferCount = 1; submit.pCommandBuffers = &command_; submit.signalSemaphoreCount = 1; submit.pSignalSemaphores = &rendered_; check(vkQueueSubmit(queue_, 1, &submit, fence_), "vkQueueSubmit"); + if (screenshot) { + check(vkWaitForFences(device_, 1, &fence_, VK_TRUE, UINT64_MAX), "vkWaitForFences readback"); + void* mapped = nullptr; + check(vkMapMemory(device_, readback_memory, 0, VK_WHOLE_SIZE, 0, &mapped), "vkMapMemory readback"); + write_bmp(screenshot_path_, static_cast(mapped)); + vkUnmapMemory(device_, readback_memory); + vkDestroyBuffer(device_, readback, nullptr); + vkFreeMemory(device_, readback_memory, nullptr); + } const auto submitted = std::chrono::steady_clock::now(); VkPresentInfoKHR present{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR}; present.waitSemaphoreCount = 1; present.pWaitSemaphores = &rendered_; @@ -1764,12 +1885,12 @@ private: indices[i_count + 4] = v_count + 1; indices[i_count + 3 + 2] = v_count + 3; - const float mask_flag = is_masked ? -1.0f : 0.0f; + const float mask_flag = is_masked ? 1.0f : 0.0f; if (!batches.empty() && batches.back().behind_3d == cmd.behind_3d && batches.back().pipeline == pipeline && batches.back().descriptor == descriptor && - batches.back().constants.light_dir[3] == mask_flag) { + batches.back().constants.params[2] == mask_flag) { batches.back().index_count += 6; } else { UiBatch batch{}; @@ -1779,10 +1900,7 @@ private: batch.descriptor = descriptor; batch.behind_3d = cmd.behind_3d; batch.constants.mvp = kIdentityMatrix; - batch.constants.tint_color = {1.0f, 1.0f, 1.0f, 1.0f}; - batch.constants.ambient_emissive = {1.0f, 1.0f, 1.0f, -1.0f}; - batch.constants.light_dir = {0.0f, 0.0f, 1.0f, mask_flag}; - batch.constants.light_diffuse = {0.0f, 0.0f, 0.0f, 0.0f}; + batch.constants.params = {0.0f, 0.0f, mask_flag, 0.0f}; batches.push_back(batch); } v_count += 4; @@ -1889,7 +2007,8 @@ private: info.surface = surface_; info.minImageCount = std::min(capabilities.minImageCount + 1, capabilities.maxImageCount ? capabilities.maxImageCount : UINT32_MAX); info.imageFormat = format.format; info.imageColorSpace = format.colorSpace; info.imageExtent = extent_; - info.imageArrayLayers = 1; info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + info.imageArrayLayers = 1; info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | + (capabilities.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_SRC_BIT); info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; info.preTransform = capabilities.currentTransform; info.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; info.presentMode = present_mode_; info.clipped = VK_TRUE; @@ -1897,6 +2016,7 @@ private: check(vkGetSwapchainImagesKHR(device_, swapchain_, &count, nullptr), "swapchain images count"); std::vector images(count); check(vkGetSwapchainImagesKHR(device_, swapchain_, &count, images.data()), "swapchain images"); + swapchain_images_ = images; for (auto image : images) { VkImageViewCreateInfo view{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; view.image = image; view.viewType = VK_IMAGE_VIEW_TYPE_2D; view.format = swapchain_format_; @@ -2255,6 +2375,10 @@ private: VkPipelineViewportStateCreateInfo viewport_info{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; viewport_info.viewportCount = 1; viewport_info.pViewports = &viewport; viewport_info.scissorCount = 1; viewport_info.pScissors = &scissor; + // D3DVIEWPORT8 changes per draw (SetViewport), so the viewport is dynamic state. + const VkDynamicState dynamic_states[] = {VK_DYNAMIC_STATE_VIEWPORT}; + VkPipelineDynamicStateCreateInfo dynamic_info{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO}; + dynamic_info.dynamicStateCount = 1; dynamic_info.pDynamicStates = dynamic_states; VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; raster.polygonMode = VK_POLYGON_MODE_FILL; @@ -2311,8 +2435,19 @@ private: blend == 2 ? VK_BLEND_FACTOR_ONE : VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; } blend_attachment.colorBlendOp = VK_BLEND_OP_ADD; - blend_attachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; - blend_attachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + // D3D8 has no separate alpha blend: SRCBLEND/DESTBLEND apply to alpha as well. + auto alpha_factor = [](VkBlendFactor factor) { + switch (factor) { + case VK_BLEND_FACTOR_SRC_COLOR: return VK_BLEND_FACTOR_SRC_ALPHA; + case VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR: return VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + case VK_BLEND_FACTOR_DST_COLOR: return VK_BLEND_FACTOR_DST_ALPHA; + case VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR: return VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA; + case VK_BLEND_FACTOR_SRC_ALPHA_SATURATE: return VK_BLEND_FACTOR_ONE; + default: return factor; + } + }; + blend_attachment.srcAlphaBlendFactor = alpha_factor(blend_attachment.srcColorBlendFactor); + blend_attachment.dstAlphaBlendFactor = alpha_factor(blend_attachment.dstColorBlendFactor); blend_attachment.alphaBlendOp = VK_BLEND_OP_ADD; } VkPipelineColorBlendStateCreateInfo blending{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; @@ -2324,6 +2459,7 @@ private: pipeline_info.pViewportState = &viewport_info; pipeline_info.pRasterizationState = &raster; pipeline_info.pMultisampleState = &multisample; pipeline_info.pDepthStencilState = &depth_stencil; pipeline_info.pColorBlendState = &blending; + pipeline_info.pDynamicState = &dynamic_info; pipeline_info.layout = layout_; pipeline_info.renderPass = pass_; VkPipeline handle = VK_NULL_HANDLE; check(vkCreateGraphicsPipelines(device_, VK_NULL_HANDLE, 1, &pipeline_info, nullptr, &handle), "vkCreateGraphicsPipelines"); @@ -2721,8 +2857,8 @@ private: const auto count = draw.positions.size() / 3; if (count > UINT32_MAX || draw.indices.size() > UINT32_MAX) throw std::runtime_error("geometry exceeds Vulkan index range"); - const std::uint32_t default_argb = - (!draw.texture0.empty() || draw.lighting != 0) ? 0xffffffffu : 0xff4fbfffu; + // D3D8 feeds opaque white for a vertex format without D3DFVF_DIFFUSE. + const std::uint32_t default_argb = 0xffffffffu; const bool has_skin = draw.bone_indices.size() >= count * 4 && draw.bone_weights.size() >= count * 4; std::vector vertices(count); @@ -2814,6 +2950,10 @@ private: VkFormat swapchain_format_ = VK_FORMAT_UNDEFINED; VkExtent2D extent_{}; std::vector image_views_; + std::vector swapchain_images_; + // --screenshot-out: read back the swapchain image of one frame into a BMP file. + std::string screenshot_path_; + std::uint64_t screenshot_frame_ = 0; VkImage depth_image_ = VK_NULL_HANDLE; VkDeviceMemory depth_memory_ = VK_NULL_HANDLE; VkImageView depth_view_ = VK_NULL_HANDLE; @@ -3221,6 +3361,7 @@ int main(int argc, char** argv) { std::size_t triangles_per_draw = 333; std::string capture_path; std::string capture_out; + std::string screenshot_out; std::string live_client_dir; std::string live_server_spec; int fake_mobs = 64; @@ -3261,6 +3402,7 @@ int main(int argc, char** argv) { synthetic_specified = true; } else if (arg == "--capture-out" && i+1 < argc) capture_out = argv[++i]; + else if (arg == "--screenshot-out" && i+1 < argc) screenshot_out = argv[++i]; else if (arg == "--live-client" && i+1 < argc) live_client_dir = argv[++i]; else if (arg == "--fake-mobs" && i+1 < argc) fake_mobs = std::stoi(argv[++i]); else if (arg == "--animate-first-draw") { @@ -3281,7 +3423,7 @@ int main(int argc, char** argv) { "[--live-server HOST:AUTH_PORT:GAME_PORT] [--width W] [--height H] " "[--draws N] [--triangles-per-draw N] [--capture FILE] [--animate-first-draw] " "[--animate-bones] [--no-vsync] [--live-client DIR] [--fake-mobs N] " - "[--gpu-skinning|--no-gpu-skinning] [--no-terrain] [--mobile] [--capture-out FILE]"); + "[--gpu-skinning|--no-gpu-skinning] [--no-terrain] [--mobile] [--capture-out FILE] [--screenshot-out FILE.bmp]"); } if (live_client_dir.empty() && !synthetic_specified) { const char* env_client = std::getenv("MT_40250_CLIENT"); @@ -3314,6 +3456,7 @@ int main(int argc, char** argv) { triangles_per_draw == 0 || triangles_per_draw > 10000) throw std::runtime_error("invalid frame/draw/triangle count"); VulkanWindow renderer(vsync, init_width, init_height); + if (!screenshot_out.empty() && frames > 0) renderer.request_screenshot(screenshot_out, std::uint64_t(frames)); if (mobile_mode) { renderer.touch_controller.set_enabled(true); renderer.touch_controller.update_screen_size(init_width, init_height); diff --git a/native_render/native.frag b/native_render/native.frag index 0c4db623..c35fb505 100644 --- a/native_render/native.frag +++ b/native_render/native.frag @@ -10,22 +10,21 @@ layout(set = 0, binding = 1) uniform sampler2D mask_sampler; layout(push_constant) uniform DrawConstants { mat4 mvp; - vec4 tint_color; - vec4 ambient_emissive; - vec4 light_dir; - vec4 light_diffuse; + vec4 params; // bone base + 1, pretransformed, UI mask, unused } draw; layout(set = 1, binding = 1, std430) readonly buffer FixedFunctionState { - uvec4 stage0_color; - uvec4 stage0_alpha; - uvec4 stage1_color; - uvec4 stage1_alpha; + uvec4 stage_color[2]; + uvec4 stage_alpha[2]; vec4 fog_color; vec4 fog_params; vec4 texture_factor; mat4 world_view; uvec4 flags; + mat4 normal_matrix; + uvec4 lighting_flags; + uvec4 material_sources; + uvec4 alpha_test; // ALPHATESTENABLE, ALPHAFUNC, ALPHAREF, unused } fixed_state; vec4 stage_arg(uint selector, vec4 diffuse, vec4 current, vec4 texel) { @@ -79,61 +78,40 @@ vec4 apply_stage(uvec4 color_state, uvec4 alpha_state, } void main() { - vec4 tex0 = texture(tex_sampler, in_uv); - if (fixed_state.flags.x == 2u) { - vec3 shadow = tex0.rgb; - if (fixed_state.flags.z != 0u && fixed_state.stage1_color.x == 4u) - shadow *= texture(mask_sampler, in_mask_uv).rgb; - shadow = mix(fixed_state.fog_color.rgb, shadow, in_fog); - if (all(greaterThanEqual(shadow, vec3(0.997)))) discard; - out_color = vec4(shadow, 1.0); - return; - } if (fixed_state.flags.x != 0u) { + // D3D8 texture stage cascade: stage 1 runs unless its COLOROP is D3DTOP_DISABLE. vec4 diffuse = in_color; - vec4 color = apply_stage(fixed_state.stage0_color, fixed_state.stage0_alpha, - diffuse, diffuse, tex0); - if (fixed_state.flags.z != 0u && fixed_state.stage1_color.x > 1u) { - vec4 tex1 = texture(mask_sampler, in_mask_uv); - color = apply_stage(fixed_state.stage1_color, fixed_state.stage1_alpha, - diffuse, color, tex1); - } - if (fixed_state.stage1_alpha.w != 0u) { - float ref = draw.ambient_emissive.a; - uint func = fixed_state.stage0_alpha.w; + vec4 color = apply_stage(fixed_state.stage_color[0], fixed_state.stage_alpha[0], + diffuse, diffuse, texture(tex_sampler, in_uv)); + if (fixed_state.stage_color[0].x > 1u && fixed_state.stage_color[1].x > 1u) + color = apply_stage(fixed_state.stage_color[1], fixed_state.stage_alpha[1], + diffuse, color, texture(mask_sampler, in_mask_uv)); + if (fixed_state.alpha_test.x != 0u) { + // D3DRS_ALPHAREF is an 8-bit reference compared against the 8-bit pixel alpha. + uint a = uint(clamp(color.a, 0.0, 1.0) * 255.0 + 0.5); + uint ref = fixed_state.alpha_test.z & 255u; + uint func = fixed_state.alpha_test.y; bool passes = func == 1u ? false : - func == 2u ? color.a < ref : - func == 3u ? abs(color.a - ref) < (0.5 / 255.0) : - func == 4u ? color.a <= ref : - func == 5u ? color.a > ref : - func == 6u ? abs(color.a - ref) >= (0.5 / 255.0) : - func == 7u ? color.a >= ref : true; + func == 2u ? a < ref : + func == 3u ? a == ref : + func == 4u ? a <= ref : + func == 5u ? a > ref : + func == 6u ? a != ref : + func == 7u ? a >= ref : true; if (!passes) discard; } color.rgb = mix(fixed_state.fog_color.rgb, color.rgb, in_fog); out_color = color; return; } - vec4 color = in_color * tex0; - if (draw.light_dir.w < -0.4) { + // 2D UI batch (UIRenderCommand): vertex color times texture, optionally clipped by a mask. + vec4 color = in_color * texture(tex_sampler, in_uv); + if (draw.params.z > 0.5) { + if (in_mask_uv.x < 0.0 || in_mask_uv.x > 1.0 || in_mask_uv.y < 0.0 || in_mask_uv.y > 1.0) + discard; vec4 mask_val = texture(mask_sampler, in_mask_uv); - if (draw.light_dir.w < -0.8) { - if (in_mask_uv.x < 0.0 || in_mask_uv.x > 1.0 || in_mask_uv.y < 0.0 || in_mask_uv.y > 1.0) { - discard; - } - color.rgb *= mask_val.rgb; - color.a = mask_val.a * in_color.a; - } else { - color.a = mask_val.a * in_color.a; - if (color.a <= 0.003) discard; - } - } else if (draw.light_dir.w > 1.001) { - // Specular sphere-map stage 1 (D3DTOP_MODULATEALPHA_ADDCOLOR) - float spec_power = draw.light_dir.w - 1.0; - vec4 spec_map = texture(mask_sampler, in_mask_uv); - color.rgb = min(color.rgb + (tex0.a * spec_power) * spec_map.rgb, vec3(1.5)); - color.a = 1.0; + color.rgb *= mask_val.rgb; + color.a = mask_val.a * in_color.a; } - if (color.a <= draw.ambient_emissive.a) discard; out_color = color; } diff --git a/native_render/native.vert b/native_render/native.vert index 07aa8add..dee669b2 100644 --- a/native_render/native.vert +++ b/native_render/native.vert @@ -19,40 +19,75 @@ layout(set = 1, binding = 0, std430) readonly buffer BonePalette { layout(push_constant) uniform DrawConstants { mat4 mvp; - vec4 tint_color; - vec4 ambient_emissive; - vec4 light_dir; - vec4 light_diffuse; + vec4 params; // bone base + 1 (0 = none), pretransformed, UI mask, unused } draw; +// D3D8 fixed-function vertex state; see FixedFunctionState in main.cpp. Lights are in camera space. layout(set = 1, binding = 1, std430) readonly buffer FixedFunctionState { - uvec4 stage0_color; - uvec4 stage0_alpha; - uvec4 stage1_color; - uvec4 stage1_alpha; + uvec4 stage_color[2]; // op, arg1, arg2, D3DTSS_TEXCOORDINDEX + uvec4 stage_alpha[2]; // op, arg1, arg2, D3DTSS_TEXTURETRANSFORMFLAGS vec4 fog_color; vec4 fog_params; vec4 texture_factor; mat4 world_view; uvec4 flags; - mat4 world; - uvec4 lighting_flags; + mat4 normal_matrix; + uvec4 lighting_flags; // LIGHTING, COLORVERTEX, vertex has diffuse, NORMALIZENORMALS + uvec4 material_sources; // DIFFUSE, AMBIENT, EMISSIVE material source, LOCALVIEWER + uvec4 alpha_test; + vec4 material_diffuse; vec4 material_ambient; vec4 material_emissive; vec4 global_ambient; - vec4 light_position_type[2]; - vec4 light_direction_range[2]; - vec4 light_diffuse[2]; - vec4 light_ambient[2]; - vec4 light_attenuation[2]; - vec4 light_spot[2]; + mat4 texture_matrix[2]; + vec4 light_position_type[8]; + vec4 light_direction_range[8]; + vec4 light_diffuse[8]; + vec4 light_ambient[8]; + vec4 light_attenuation[8]; + vec4 light_spot[8]; } fixed_state; +// D3DMCS_COLOR1 picks the vertex diffuse color when COLORVERTEX is on and the vertex has one. +vec4 material_color(uint source, vec4 material) { + if (fixed_state.lighting_flags.y != 0u && fixed_state.lighting_flags.z != 0u && source == 1u) + return in_color; + return material; +} + +// D3D8 texture coordinate processing: D3DTSS_TEXCOORDINDEX generation, then D3DTS_TEXTUREn. +vec2 stage_texcoord(int stage, bool pretransformed, vec3 eye, vec3 n) { + uint tci = fixed_state.stage_color[stage].w; + uint gen = tci & 0xFFFF0000u; + uint set_index = tci & 0xFFFFu; + vec4 coord = vec4(0.0, 0.0, 1.0, 0.0); + if (gen == 0u || pretransformed) { + if (set_index == 0u) coord.xy = in_uv; + else if (set_index == 1u) coord.xy = in_mask_uv; + } else if (gen == 0x10000u) { // CAMERASPACENORMAL + coord = vec4(n, 1.0); + } else if (gen == 0x20000u) { // CAMERASPACEPOSITION + coord = vec4(eye, 1.0); + } else if (gen == 0x30000u) { // CAMERASPACEREFLECTIONVECTOR + vec3 e = fixed_state.material_sources.w != 0u + ? (length(eye) > 0.0 ? -normalize(eye) : vec3(0.0)) : vec3(0.0, 0.0, -1.0); + coord = vec4(2.0 * dot(e, n) * n - e, 1.0); + } + uint transform_flags = fixed_state.stage_alpha[stage].w; + uint elements = transform_flags & 0xFFu; + vec4 r = coord; + if (elements != 0u) + r = fixed_state.texture_matrix[stage] * coord; + if ((transform_flags & 0x100u) != 0u && elements >= 2u && elements <= 4u && r[elements - 1u] != 0.0) + r.xy /= r[elements - 1u]; + return r.xy; +} + void main() { vec3 pos = in_position; vec3 nrm = in_normal; - if (draw.light_diffuse.w > 0.5) { - uint bone_base = uint(draw.light_diffuse.w - 0.5); + if (draw.params.x > 0.5) { + uint bone_base = uint(draw.params.x - 0.5); vec3 skinned_pos = vec3(0.0); vec3 skinned_nrm = vec3(0.0); float total_w = 0.0; @@ -70,72 +105,74 @@ void main() { nrm = skinned_nrm; } } + bool pretransformed = draw.params.y > 0.5; // D3D row-vector matrices are uploaded row-major. GLSL reads the bytes as their transpose. gl_Position = draw.mvp * vec4(pos, 1.0); - if (draw.light_diffuse.w < -0.5) { + if (pretransformed) { float clip_w = in_rhw > 0.0 ? 1.0 / in_rhw : 1.0; gl_Position.xyz *= clip_w; gl_Position.w = clip_w; } gl_Position.y = -gl_Position.y; - vec3 n = length(nrm) > 1e-4 ? normalize(nrm) : vec3(0.0, 0.0, 1.0); + + // A vertex without D3DFVF_DIFFUSE carries opaque white (upload_geometry). out_color = in_color; - if (fixed_state.lighting_flags.x != 0u) { - vec3 world_pos = (fixed_state.world * vec4(pos, 1.0)).xyz; - vec3 world_normal = normalize(mat3(fixed_state.world) * n); - vec3 mat_diffuse = (fixed_state.lighting_flags.y != 0u && fixed_state.lighting_flags.w == 1u) - ? in_color.rgb : draw.tint_color.rgb; - vec3 mat_ambient = (fixed_state.lighting_flags.y != 0u && fixed_state.lighting_flags.z == 1u) - ? in_color.rgb : fixed_state.material_ambient.rgb; + if (fixed_state.flags.x == 0u) { + // 2D UI batch. + out_uv = in_uv; + out_mask_uv = in_mask_uv; + out_fog = 1.0; + return; + } + + vec3 eye = (fixed_state.world_view * vec4(pos, 1.0)).xyz; + vec3 n = mat3(fixed_state.normal_matrix) * nrm; + if (fixed_state.lighting_flags.w != 0u && length(n) > 0.0) + n = normalize(n); + + if (fixed_state.lighting_flags.x != 0u && !pretransformed) { + vec4 mat_diffuse = material_color(fixed_state.material_sources.x, fixed_state.material_diffuse); + vec4 mat_ambient = material_color(fixed_state.material_sources.y, fixed_state.material_ambient); + vec4 mat_emissive = material_color(fixed_state.material_sources.z, fixed_state.material_emissive); vec3 ambient_sum = fixed_state.global_ambient.rgb; vec3 diffuse_sum = vec3(0.0); - for (int i = 0; i < 2; ++i) { + for (int i = 0; i < 8; ++i) { int type = int(fixed_state.light_position_type[i].w + 0.5); if (type == 0) continue; vec3 L; float strength = 1.0; - if (type == 3) { - L = normalize(-fixed_state.light_direction_range[i].xyz); - } else { - vec3 to_light = fixed_state.light_position_type[i].xyz - world_pos; - float distance_to_light = length(to_light); - if (distance_to_light > fixed_state.light_direction_range[i].w || distance_to_light < 0.0001) + if (type == 3) { // D3DLIGHT_DIRECTIONAL + L = -fixed_state.light_direction_range[i].xyz; + } else { // D3DLIGHT_POINT / D3DLIGHT_SPOT + vec3 to_light = fixed_state.light_position_type[i].xyz - eye; + float d = length(to_light); + if (d > fixed_state.light_direction_range[i].w) continue; - L = to_light / distance_to_light; + L = d > 0.0 ? to_light / d : vec3(0.0); vec4 attenuation = fixed_state.light_attenuation[i]; - float denominator = attenuation.x + attenuation.y * distance_to_light + - attenuation.z * distance_to_light * distance_to_light; - strength = denominator > 0.0001 ? min(1.0 / denominator, 1.0) : 1.0; + float denominator = attenuation.x + attenuation.y * d + attenuation.z * d * d; + strength = denominator != 0.0 ? 1.0 / denominator : 1.0; if (type == 2) { - vec3 spot_dir = normalize(fixed_state.light_direction_range[i].xyz); - float cosine = dot(-L, spot_dir); - float inner = cos(fixed_state.light_spot[i].x * 0.5); - float outer = cos(fixed_state.light_spot[i].y * 0.5); - float cone = clamp((cosine - outer) / max(inner - outer, 0.0001), 0.0, 1.0); - strength *= pow(cone, max(attenuation.w, 0.0001)); + float rho = dot(-L, fixed_state.light_direction_range[i].xyz); + float cos_theta = fixed_state.light_spot[i].x; + float cos_phi = fixed_state.light_spot[i].y; + float spot = rho > cos_theta ? 1.0 : (rho <= cos_phi ? 0.0 : + pow((rho - cos_phi) / (cos_theta - cos_phi), attenuation.w)); + strength *= spot; } } ambient_sum += fixed_state.light_ambient[i].rgb * strength; - diffuse_sum += fixed_state.light_diffuse[i].rgb * max(dot(world_normal, L), 0.0) * strength; + diffuse_sum += fixed_state.light_diffuse[i].rgb * max(dot(n, L), 0.0) * strength; } - vec3 lit = fixed_state.material_emissive.rgb + mat_ambient * ambient_sum + mat_diffuse * diffuse_sum; - float alpha = (fixed_state.lighting_flags.y != 0u && fixed_state.lighting_flags.w == 1u) - ? in_color.a : draw.tint_color.a; - out_color = vec4(clamp(lit, 0.0, 1.0), alpha); - } - out_uv = in_uv; - if (draw.light_dir.w > 1.001) { - vec3 view_normal = normalize(mat3(fixed_state.world_view) * n); - vec3 view_pos = (fixed_state.world_view * vec4(pos, 1.0)).xyz; - vec3 view_dir = normalize(-view_pos); - vec3 reflected = reflect(-view_dir, view_normal); - out_mask_uv = reflected.xy * vec2(0.5, -0.5) + vec2(0.5); - } else { - out_mask_uv = in_mask_uv; + vec3 lit = mat_emissive.rgb + mat_ambient.rgb * ambient_sum + mat_diffuse.rgb * diffuse_sum; + out_color = vec4(clamp(lit, 0.0, 1.0), clamp(mat_diffuse.a, 0.0, 1.0)); } + + out_uv = stage_texcoord(0, pretransformed, eye, n); + out_mask_uv = stage_texcoord(1, pretransformed, eye, n); + out_fog = 1.0; - if (fixed_state.flags.x != 0u && fixed_state.flags.w != 0u && draw.light_diffuse.w >= -0.5) { - vec3 eye = (fixed_state.world_view * vec4(pos, 1.0)).xyz; + if (fixed_state.flags.w != 0u && !pretransformed) { float distance_to_eye = fixed_state.fog_params.w > 0.5 ? length(eye) : abs(eye.z); if (fixed_state.flags.w == 3u) { float span = fixed_state.fog_params.y - fixed_state.fog_params.x;