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
+253 -110
View File
@@ -63,38 +63,40 @@ struct Vertex {
struct PushConstants {
std::array<float, 16> mvp{};
std::array<float, 4> tint_color{1.0f, 1.0f, 1.0f, 1.0f};
std::array<float, 4> ambient_emissive{1.0f, 1.0f, 1.0f, -1.0f};
std::array<float, 4> light_dir{0.0f, 0.0f, 1.0f, 0.0f};
std::array<float, 4> 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<float, 4> 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<std::uint32_t, 4> stage0_color{}; // op, arg1, arg2, unused
std::array<std::uint32_t, 4> stage0_alpha{}; // op, arg1, arg2, unused
std::array<std::uint32_t, 4> stage1_color{};
std::array<std::uint32_t, 4> stage1_alpha{};
std::array<std::array<std::uint32_t, 4>, 2> stage_color{}; // op, arg1, arg2, D3DTSS_TEXCOORDINDEX
std::array<std::array<std::uint32_t, 4>, 2> stage_alpha{}; // op, arg1, arg2, D3DTSS_TEXTURETRANSFORMFLAGS
std::array<float, 4> fog_color{};
std::array<float, 4> fog_params{}; // start, end, density, range enabled
std::array<float, 4> texture_factor{};
std::array<float, 16> world_view{};
std::array<std::uint32_t, 4> flags{}; // fixed function, texture0, texture1, fog mode
std::array<float, 16> world{};
std::array<std::uint32_t, 4> lighting_flags{}; // enabled, COLORVERTEX, ambient source, diffuse source
std::array<float, 16> normal_matrix{}; // inverse transpose of world * view
std::array<std::uint32_t, 4> lighting_flags{}; // LIGHTING, COLORVERTEX, vertex has diffuse, NORMALIZENORMALS
std::array<std::uint32_t, 4> material_sources{}; // DIFFUSE, AMBIENT, EMISSIVE source, LOCALVIEWER
std::array<std::uint32_t, 4> alpha_test{}; // ALPHATESTENABLE, ALPHAFUNC, ALPHAREF, unused
std::array<float, 4> material_diffuse{};
std::array<float, 4> material_ambient{};
std::array<float, 4> material_emissive{};
std::array<float, 4> global_ambient{};
std::array<std::array<float, 4>, 2> light_position_type{};
std::array<std::array<float, 4>, 2> light_direction_range{};
std::array<std::array<float, 4>, 2> light_diffuse{};
std::array<std::array<float, 4>, 2> light_ambient{};
std::array<std::array<float, 4>, 2> light_attenuation{};
std::array<std::array<float, 4>, 2> light_spot{};
std::array<std::array<float, 16>, 2> texture_matrix{};
// Lights in camera space. position.w = D3DLIGHTTYPE (0 = disabled).
std::array<std::array<float, 4>, 8> light_position_type{};
std::array<std::array<float, 4>, 8> light_direction_range{};
std::array<std::array<float, 4>, 8> light_diffuse{};
std::array<std::array<float, 4>, 8> light_ambient{};
std::array<std::array<float, 4>, 8> light_attenuation{}; // a0, a1, a2, falloff
std::array<std::array<float, 4>, 8> light_spot{}; // cos(theta / 2), cos(phi / 2)
};
static_assert(sizeof(FixedFunctionState) == 512);
static_assert(sizeof(FixedFunctionState) == 1264);
constexpr std::array<float, 16> kIdentityMatrix = {
1.0f, 0.0f, 0.0f, 0.0f,
@@ -116,11 +118,6 @@ std::array<float, 16> 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<float, 4> unpack_argb(std::uint32_t argb) {
return {
float((argb >> 16) & 255) / 255.0f,
@@ -129,93 +126,77 @@ std::array<float, 4> 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<float, 16> normal_matrix(const std::array<float, 16>& 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<float, 16> 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<float, 3> 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<std::uint8_t> 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<const char*>(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<std::uint32_t>(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<std::uint8_t>(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<std::uint32_t>(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, &current_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, &region);
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<const std::uint8_t*>(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<VkImage> 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<Vertex> vertices(count);
@@ -2814,6 +2950,10 @@ private:
VkFormat swapchain_format_ = VK_FORMAT_UNDEFINED;
VkExtent2D extent_{};
std::vector<VkImageView> image_views_;
std::vector<VkImage> 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);