Files
metin2-client/src/host/ui_batches.cpp
T
shenleiandClaude Opus 5.5 22d534c5eb Split vulkan_window.cpp by responsibility and render() into phases
VulkanWindow's 3000-line TU becomes sdl_window.cpp (window, events,
cursor, screenshot BMP), vk_device.cpp (device, swapchain, MSAA,
framebuffers), vk_pipelines.cpp (render passes, pipelines),
vk_resources.cpp (buffers, render targets, geometry), vk_textures.cpp
(textures, samplers, descriptors), vk_frame.cpp (render) and
ui_batches.cpp; vulkan_window.cpp keeps construction and timing stats.
Function bodies move verbatim.

render() now reads fence/acquire -> prepare_draws -> prepare_ui ->
write_draw_states -> record_passes -> record_readbacks -> submit ->
finish_readbacks -> present, with FrameDraws / Readback carrying the
locals that used to live across 500 lines.

Verified: login-screen screenshot byte-identical to HEAD; game-world
draw/vertex/index/UI counts identical; ctest, port_gate macos+android,
Android build pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:54:36 +09:00

195 lines
8.5 KiB
C++

// VulkanWindow::build_ui_batches: UIRenderCommand quads into the frame UI buffer, batched by texture and blend.
#include "vulkan_window.h"
#include <algorithm>
#include <cmath>
#include <stdexcept>
namespace mt_host {
void VulkanWindow::build_ui_batches(
std::uint32_t ui_width,
std::uint32_t ui_height,
const std::vector<UIRenderCommand>& commands,
const std::unordered_map<std::string, std::vector<std::uint8_t>>& capture_textures,
std::vector<UiBatch>& batches,
std::size_t& out_quad_count) {
ensure_ui_capacity(commands.size());
auto* vertices = reinterpret_cast<Vertex*>(f_->ui_mapped);
auto* indices = reinterpret_cast<std::uint32_t*>(f_->ui_mapped + f_->ui_vertex_bytes);
std::uint32_t v_count = 0;
std::uint32_t i_count = 0;
const float inv_w = 2.0f / float(ui_width);
const float inv_h = 2.0f / float(ui_height);
for (const auto& cmd : commands) {
if (v_count + 4 > f_->ui_vertex_capacity || i_count + 6 > f_->ui_index_capacity)
throw std::runtime_error("UI command buffer capacity exceeded");
if (cmd.kind == UIRenderCommand::Text) continue;
float x1 = cmd.x1, y1 = cmd.y1, x2 = cmd.x2, y2 = cmd.y2;
float su = cmd.su, sv = cmd.sv, eu = cmd.eu, ev = cmd.ev;
const bool has_clip = cmd.clip_x2 > cmd.clip_x1 && cmd.clip_y2 > cmd.clip_y1;
float qx[4], qy[4], u[4], v[4], mu[4] = {}, mv[4] = {};
std::array<float, 4> top_color = unpack_argb(cmd.argb);
std::array<float, 4> bot_color =
cmd.kind == UIRenderCommand::GradientBar ? unpack_argb(cmd.end_argb) : top_color;
if (top_color[3] <= 0.001f && bot_color[3] <= 0.001f) continue;
if (cmd.kind == UIRenderCommand::Line) {
const float dx = x2 - x1, dy = y2 - y1;
const float len = std::sqrt(dx * dx + dy * dy);
if (len < 0.1f) continue;
const float nx = -dy / len * 0.5f;
const float ny = dx / len * 0.5f;
qx[0] = x1 + nx; qy[0] = y1 + ny;
qx[1] = x2 + nx; qy[1] = y2 + ny;
qx[2] = x1 - nx; qy[2] = y1 - ny;
qx[3] = x2 - nx; qy[3] = y2 - ny;
for (int k = 0; k < 4; ++k) { u[k] = 0.0f; v[k] = 0.0f; }
} else if (cmd.kind == UIRenderCommand::Image && cmd.quad) {
const bool axis_aligned =
std::fabs(cmd.qx[0] - cmd.qx[2]) < 1e-3f &&
std::fabs(cmd.qx[1] - cmd.qx[3]) < 1e-3f &&
std::fabs(cmd.qy[0] - cmd.qy[1]) < 1e-3f &&
std::fabs(cmd.qy[2] - cmd.qy[3]) < 1e-3f;
for (int k = 0; k < 4; ++k) {
mu[k] = cmd.mu[k];
mv[k] = cmd.mv[k];
}
if (has_clip && axis_aligned) {
const float ox1 = cmd.qx[0], oy1 = cmd.qy[0], ox2 = cmd.qx[3], oy2 = cmd.qy[3];
const float cx1 = std::max(ox1, cmd.clip_x1);
const float cy1 = std::max(oy1, cmd.clip_y1);
const float cx2 = std::min(ox2, cmd.clip_x2);
const float cy2 = std::min(oy2, cmd.clip_y2);
if (cx2 <= cx1 || cy2 <= cy1) continue;
float msu = cmd.mu[0], meu = cmd.mu[1];
float msv = cmd.mv[0], mev = cmd.mv[2];
if (ox2 > ox1) {
const float t0 = (cx1 - ox1) / (ox2 - ox1);
const float t1 = (cx2 - ox1) / (ox2 - ox1);
const float du = eu - su;
su = cmd.su + du * t0;
eu = cmd.su + du * t1;
const float mdu = meu - msu;
msu = cmd.mu[0] + mdu * t0;
meu = cmd.mu[0] + mdu * t1;
}
if (oy2 > oy1) {
const float t0 = (cy1 - oy1) / (oy2 - oy1);
const float t1 = (cy2 - oy1) / (oy2 - oy1);
const float dv = ev - sv;
sv = cmd.sv + dv * t0;
ev = cmd.sv + dv * t1;
const float mdv = mev - msv;
msv = cmd.mv[0] + mdv * t0;
mev = cmd.mv[0] + mdv * t1;
}
qx[0] = cx1; qy[0] = cy1;
qx[1] = cx2; qy[1] = cy1;
qx[2] = cx1; qy[2] = cy2;
qx[3] = cx2; qy[3] = cy2;
mu[0] = msu; mv[0] = msv;
mu[1] = meu; mv[1] = msv;
mu[2] = msu; mv[2] = mev;
mu[3] = meu; mv[3] = mev;
} else {
if (has_clip && (x2 <= cmd.clip_x1 || x1 >= cmd.clip_x2 || y2 <= cmd.clip_y1 || y1 >= cmd.clip_y2))
continue;
for (int k = 0; k < 4; ++k) {
qx[k] = cmd.qx[k];
qy[k] = cmd.qy[k];
}
}
u[0] = su; v[0] = sv;
u[1] = eu; v[1] = sv;
u[2] = su; v[2] = ev;
u[3] = eu; v[3] = ev;
} else if (cmd.kind == UIRenderCommand::Bar && cmd.quad) {
// An untextured polygon piece (CPythonGraphic::RenderCoolTimeBox's fan triangles).
if (has_clip && (x2 <= cmd.clip_x1 || x1 >= cmd.clip_x2 || y2 <= cmd.clip_y1 || y1 >= cmd.clip_y2))
continue;
for (int k = 0; k < 4; ++k) {
qx[k] = cmd.qx[k];
qy[k] = cmd.qy[k];
u[k] = 0.0f;
v[k] = 0.0f;
}
} else {
if (has_clip) {
x1 = std::max(x1, cmd.clip_x1);
y1 = std::max(y1, cmd.clip_y1);
x2 = std::min(x2, cmd.clip_x2);
y2 = std::min(y2, cmd.clip_y2);
}
if (x2 <= x1 || y2 <= y1) continue;
qx[0] = x1; qy[0] = y1;
qx[1] = x2; qy[1] = y1;
qx[2] = x1; qy[2] = y2;
qx[3] = x2; qy[3] = y2;
u[0] = su; v[0] = sv;
u[1] = eu; v[1] = sv;
u[2] = su; v[2] = ev;
u[3] = eu; v[3] = ev;
}
const std::string& tex_name = cmd.kind == UIRenderCommand::Image ? cmd.text : std::string();
const std::string& mask_name = cmd.kind == UIRenderCommand::Image ? cmd.mask : std::string();
const bool is_masked = !mask_name.empty();
// CGraphicExpandedImageInstance: SCREEN/COLOR_DODGE use
// INVDESTCOLOR + ONE; MODULATE uses ZERO + SRCCOLOR.
const std::uint8_t blend_mode = cmd.kind == UIRenderCommand::Image
? ((cmd.blend == 1 || cmd.blend == 2) ? 0x28 : (cmd.blend == 3 ? 0x31 : 1))
: 1;
const VkPipeline pipeline = get_pipeline(0, 0, blend_mode);
const VkDescriptorSet descriptor = get_ui_texture_descriptor(tex_name, mask_name, capture_textures);
for (int k = 0; k < 4; ++k) {
Vertex& vert = vertices[v_count + k];
vert.position[0] = qx[k] * inv_w - 1.0f;
vert.position[1] = 1.0f - qy[k] * inv_h;
vert.position[2] = 0.0f;
vert.normal[0] = 0.0f; vert.normal[1] = 0.0f; vert.normal[2] = 1.0f;
vert.uv[0] = u[k]; vert.uv[1] = v[k];
const auto& col = (k < 2) ? top_color : bot_color;
vert.color[0] = col[0]; vert.color[1] = col[1]; vert.color[2] = col[2]; vert.color[3] = col[3];
vert.joints[0] = vert.joints[1] = vert.joints[2] = vert.joints[3] = 0;
vert.weights[0] = 1.0f; vert.weights[1] = vert.weights[2] = vert.weights[3] = 0.0f;
vert.mask_uv[0] = mu[k]; vert.mask_uv[1] = mv[k];
vert.rhw = 1.0f;
}
indices[i_count + 0] = v_count + 0;
indices[i_count + 1] = v_count + 1;
indices[i_count + 2] = v_count + 2;
indices[i_count + 3] = v_count + 2;
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;
if (!batches.empty() &&
batches.back().behind_3d == cmd.behind_3d &&
batches.back().pipeline == pipeline &&
batches.back().descriptor == descriptor &&
batches.back().constants.params[2] == mask_flag) {
batches.back().index_count += 6;
} else {
UiBatch batch{};
batch.first_index = i_count;
batch.index_count = 6;
batch.pipeline = pipeline;
batch.descriptor = descriptor;
batch.behind_3d = cmd.behind_3d;
batch.constants.mvp = kIdentityMatrix;
batch.constants.params = {0.0f, 0.0f, mask_flag, 0.0f};
batches.push_back(batch);
}
v_count += 4;
i_count += 6;
++out_quad_count;
}
}
} // namespace mt_host