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
@@ -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
@@ -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);
|
||||
|
||||
+31
-53
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user