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 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-28 12:54:13 +09:00
co-authored by Claude Opus 5.5
parent 425e6f37fb
commit 5285ca85fd
11 changed files with 585 additions and 321 deletions
+2 -2
View File
@@ -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{};
@@ -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<CpuSurface*>(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<std::uint64_t>(reinterpret_cast<std::uintptr_t>(m_textures[0])));
if (texgen_cam_pos1)
key = mix(key, static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(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<Render3DDraw>& 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<float>& 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<float>* 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];
}
}
@@ -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:<id>@<revision>"; 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<float>& out);
struct GpuSkinSubrangeView {
std::uint64_t source_mesh_key = 0;
std::uint32_t mesh_base_vertex = 0;
+16 -3
View File
@@ -403,6 +403,8 @@ Array Metin2PythonHost::render3d_draws() {
out.resize(static_cast<int64_t>(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<int64_t>(draw.geometry_key);
item["geometry_revision"] = static_cast<int64_t>(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<int64_t>(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<float> 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<int32_t>(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)