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
+32 -4
View File
@@ -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<Render3DDraw> draws;
std::unordered_map<std::string, std::vector<std::uint8_t>> 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<std::uint32_t>(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<const char*>(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<const char*>(draw.border_color), sizeof(draw.border_color));
file.write(reinterpret_cast<const char*>(draw.texcoord_index), sizeof(draw.texcoord_index));
file.write(reinterpret_cast<const char*>(draw.texture_transform_flags), sizeof(draw.texture_transform_flags));
file.write(reinterpret_cast<const char*>(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<char*>(draw.mip_filter), sizeof(draw.mip_filter));
}
if (version >= 7) {
file.read(reinterpret_cast<char*>(draw.lights), sizeof(draw.lights));
file.read(reinterpret_cast<char*>(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<char*>(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<char*>(draw.border_color), sizeof(draw.border_color));
file.read(reinterpret_cast<char*>(draw.texcoord_index), sizeof(draw.texcoord_index));
file.read(reinterpret_cast<char*>(draw.texture_transform_flags), sizeof(draw.texture_transform_flags));
file.read(reinterpret_cast<char*>(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;
+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);
+31 -53
View File
@@ -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;
}
+98 -61
View File
@@ -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;