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:
co-authored by
Claude Opus 5.5
parent
425e6f37fb
commit
5285ca85fd
+253
-110
@@ -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, ¤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<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);
|
||||
|
||||
Reference in New Issue
Block a user