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>
This commit is contained in:
shenlei
2026-09-29 21:54:36 +09:00
co-authored by Claude Opus 5.5
parent eab5938072
commit 22d534c5eb
13 changed files with 2584 additions and 2429 deletions
@@ -33,7 +33,7 @@
- `src/port/<Lib>/<File>.{h,cpp}` — mirror of each 40250 `logic`/`python` unit (see SKILL.md "Code layout"). - `src/port/<Lib>/<File>.{h,cpp}` — mirror of each 40250 `logic`/`python` unit (see SKILL.md "Code layout").
- `src/platform/` — implementations of the mirrored 40250 platform classes (D3D8 recorder, Granny, - `src/platform/` — implementations of the mirrored 40250 platform classes (D3D8 recorder, Granny,
Miles, SpeedTree stand-in, window/input, ScriptLib boot). Miles, SpeedTree stand-in, window/input, ScriptLib boot).
- `src/host/` — SDL3 + Vulkan host (`main.cpp` entry, `live_client.cpp` loop, `vulkan_window.cpp` renderer); `src/gr2/` — Granny reader; `src/codepage/` — code pages. - `src/host/` — SDL3 + Vulkan host (`main.cpp` entry, `live_client.cpp` loop, `vulkan_window.h` renderer split across `vulkan_window.cpp`/`vk_*.cpp`/`sdl_window.cpp`/`ui_batches.cpp`); `src/gr2/` — Granny reader; `src/codepage/` — code pages.
- `third_party/` — vendored cpython 2.7.18, cryptopp, freetype, minilzo. - `third_party/` — vendored cpython 2.7.18, cryptopp, freetype, minilzo.
- `tests/port/` — port tests and the fake login server; `tests/gr2/` — Granny reader tests. - `tests/port/` — port tests and the fake login server; `tests/gr2/` — Granny reader tests.
- `android/` — SDLActivity shell, `android/build.sh`, `android/push-client.sh`. - `android/` — SDLActivity shell, `android/build.sh`, `android/push-client.sh`.
@@ -29,8 +29,8 @@
"status": "ADAPTED", "status": "ADAPTED",
"impl": [ "impl": [
"src/host/dxt.cpp:decode", "src/host/dxt.cpp:decode",
"src/host/vulkan_window.cpp:get_gpu_texture", "src/host/vk_textures.cpp:get_gpu_texture",
"src/host/vulkan_window.cpp:upload_texture" "src/host/vk_textures.cpp:upload_texture"
], ],
"test": "tests/port/dxt_decode_test.cpp", "test": "tests/port/dxt_decode_test.cpp",
"note": "The platform CreateDDSTexture member is a stub; its level logic lives in the texture decode/upload the renderer does. mipmapCount = max(1, min(dwMipMapCount, MAX_MIPLEVELS=12)), level i read at sum(dwLinearSize>>2k) with CDXTCImage::Copy's dwLinearSize>>2i bytes (the rest zero, uninitialised in 40250), decoded to RGBA8 (no GPU DXT) and uploaded as file levels, not regenerated. Divergence: without DDSD_MIPMAPCOUNT, or with DDSD_PITCH, 40250 creates the extra levels uninitialised; here that is one level. IsLowTextureMemory's uTexBias level skip is not applied (the port's ms_isLowTextureMemory is false). The far-terrain look NEEDS_LIVE." "note": "The platform CreateDDSTexture member is a stub; its level logic lives in the texture decode/upload the renderer does. mipmapCount = max(1, min(dwMipMapCount, MAX_MIPLEVELS=12)), level i read at sum(dwLinearSize>>2k) with CDXTCImage::Copy's dwLinearSize>>2i bytes (the rest zero, uninitialised in 40250), decoded to RGBA8 (no GPU DXT) and uploaded as file levels, not regenerated. Divergence: without DDSD_MIPMAPCOUNT, or with DDSD_PITCH, 40250 creates the extra levels uninitialised; here that is one level. IsLowTextureMemory's uTexBias level skip is not applied (the port's ms_isLowTextureMemory is false). The far-terrain look NEEDS_LIVE."
@@ -39,7 +39,7 @@
"status": "ADAPTED", "status": "ADAPTED",
"impl": [ "impl": [
"src/platform/EterLib/GrpImageTexture.cpp:CGraphicImageTexture::CreateFromMemoryFile", "src/platform/EterLib/GrpImageTexture.cpp:CGraphicImageTexture::CreateFromMemoryFile",
"src/host/vulkan_window.cpp:get_gpu_texture" "src/host/vk_textures.cpp:get_gpu_texture"
], ],
"test": "tests/port/dxt_decode_test.cpp", "test": "tests/port/dxt_decode_test.cpp",
"note": "The platform body records only the size and file name; the renderer decodes the bytes. 40250 dispatch is kept: a DXT DDS takes the CreateDDSTexture levels, anything else takes D3DXCreateTextureFromFileInMemoryEx(MipLevels=D3DX_DEFAULT), i.e. a full generated chain (vkCmdBlitImage linear, instead of D3DX's box filter). The invented is_ui_tex rule (no mips for icon/, ui/, locale/ and textures <=128px) was removed. Memory textures (mem:, 40250 CreateTexture levels=1) get one level. GRAPHICS_CAPS_HALF_SIZE_IMAGE/low-memory A4R4G4B4 re-encode is not applied." "note": "The platform body records only the size and file name; the renderer decodes the bytes. 40250 dispatch is kept: a DXT DDS takes the CreateDDSTexture levels, anything else takes D3DXCreateTextureFromFileInMemoryEx(MipLevels=D3DX_DEFAULT), i.e. a full generated chain (vkCmdBlitImage linear, instead of D3DX's box filter). The invented is_ui_tex rule (no mips for icon/, ui/, locale/ and textures <=128px) was removed. Memory textures (mem:, 40250 CreateTexture levels=1) get one level. GRAPHICS_CAPS_HALF_SIZE_IMAGE/low-memory A4R4G4B4 re-encode is not applied."
+5 -1
View File
@@ -64,7 +64,11 @@ src/platform/ScriptLib/PythonBoot.cpp # 解释器启动、RunMainScript
third_party/cpython-2.7.18/ # 静态库 third_party/cpython-2.7.18/ # 静态库
src/host/main.cpp # SDL3 宿主入口:命令行、合成基准 src/host/main.cpp # SDL3 宿主入口:命令行、合成基准
src/host/live_client.cpp # run_live_client:40250 客户端主循环 src/host/live_client.cpp # run_live_client:40250 客户端主循环
src/host/vulkan_window.cpp # SDL 窗口/事件 + Vulkan 渲染器(回放 Render3DDraw / UIRenderCommand) src/host/vulkan_window.{h,cpp} # VulkanWindow:构造/析构 + 帧计时统计(回放 Render3DDraw / UIRenderCommand)
src/host/sdl_window.cpp # SDL 窗口/事件/光标/屏幕键盘/截图 BMP
src/host/vk_{device,pipelines,resources,textures}.cpp # 交换链 / 渲染通道+管线 / 缓冲+RT+几何 / 纹理+采样器+描述符
src/host/vk_frame.cpp # render():prepare_draws → prepare_ui → write_draw_states → record_passes → readback
src/host/ui_batches.cpp # UIRenderCommand → UiBatch 合批
src/host/frame_policy.cpp # 帧率策略(FrameRatePolicy) src/host/frame_policy.cpp # 帧率策略(FrameRatePolicy)
src/host/native_audio.cpp # MilesLib AudioCommand → SDL 音频流 src/host/native_audio.cpp # MilesLib AudioCommand → SDL 音频流
src/host/audio_decode.cpp # .wav(手写 PCM 解析)/.mp3(dr_mp3)→ 44.1 kHz 立体声 float,全平台同一路径 src/host/audio_decode.cpp # .wav(手写 PCM 解析)/.mp3(dr_mp3)→ 44.1 kHz 立体声 float,全平台同一路径
+7
View File
@@ -23,6 +23,13 @@ set(MT_NATIVE_RENDER_SOURCES
render_state.cpp render_state.cpp
texture_decode.cpp texture_decode.cpp
vulkan_window.cpp vulkan_window.cpp
sdl_window.cpp
ui_batches.cpp
vk_device.cpp
vk_frame.cpp
vk_pipelines.cpp
vk_resources.cpp
vk_textures.cpp
stb_image_impl.cpp stb_image_impl.cpp
dxt.cpp dxt.cpp
) )
+382
View File
@@ -0,0 +1,382 @@
// The SDL side of VulkanWindow: events and input forwarding, cursor, screen keyboard, logical size, screenshots on disk.
#include "vulkan_window.h"
#include "input_keymap.h"
#include "texture_decode.h"
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
#include "platform/ScriptLib/PythonBoot.h"
#endif
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <optional>
#include <sstream>
namespace mt_host {
namespace {
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
SDL_Cursor* load_game_cursor(int shape) {
static constexpr std::array<const char*, 15> images = {
"cursor.sub", "cursor_attack.sub", "cursor_attack.sub", "cursor_talk.sub",
"cursor_no.sub", "cursor_pick.sub", "cursor_door.sub", "cursor_chair.sub",
"cursor_chair.sub", "cursor_buy.sub", "cursor_sell.sub",
"cursor_camera_rotate.sub", "cursor_hsize.sub", "cursor_vsize.sub", "cursor_hvsize.sub"};
if (shape < 0 || shape >= static_cast<int>(images.size())) return nullptr;
const std::string sub_path = std::string("d:/ymir work/ui/cursor/") + images[shape];
std::vector<std::uint8_t> sub_bytes;
if (!read_live_pack_texture(sub_path, sub_bytes)) return nullptr;
std::unordered_map<std::string, std::string> tokens;
std::istringstream input(std::string(sub_bytes.begin(), sub_bytes.end()));
std::string line;
while (std::getline(input, line)) {
std::istringstream fields(line);
std::string key, value;
if (!(fields >> key >> value)) continue;
if (!value.empty() && value.front() == '"') {
value.erase(0, 1);
if (!value.empty() && value.back() == '"') value.pop_back();
}
std::transform(key.begin(), key.end(), key.begin(), [](unsigned char ch) { return std::tolower(ch); });
tokens[key] = value;
}
if (tokens["title"] != "subimage" || tokens["image"].empty()) return nullptr;
const std::string image_path = tokens["version"] == "2.0"
? std::string("d:/ymir work/ui/cursor/") + tokens["image"]
: std::string("d:/ymir work/ui/") + tokens["image"];
std::vector<std::uint8_t> image_bytes;
if (!read_live_pack_texture(image_path, image_bytes)) return nullptr;
const auto image = decode_texture_bytes(image_bytes.data(), image_bytes.size());
if (!image.ok()) return nullptr;
const int left = std::atoi(tokens["left"].c_str());
const int top = std::atoi(tokens["top"].c_str());
const int right = std::atoi(tokens["right"].c_str());
const int bottom = std::atoi(tokens["bottom"].c_str());
if (left < 0 || top < 0 || right <= left || bottom <= top ||
right > image.w || bottom > image.h) return nullptr;
const int width = right - left, height = bottom - top;
std::vector<std::uint8_t> pixels(std::size_t(width) * height * 4);
for (int y = 0; y < height; ++y)
std::memcpy(pixels.data() + std::size_t(y) * width * 4,
image.rgba.data() + (std::size_t(top + y) * image.w + left) * 4,
std::size_t(width) * 4);
SDL_Surface* surface = SDL_CreateSurfaceFrom(width, height, SDL_PIXELFORMAT_RGBA32,
pixels.data(), width * 4);
if (!surface) return nullptr;
const int hot_x = shape >= 12 ? std::min(16, width - 1) : 0;
const int hot_y = shape >= 12 ? std::min(16, height - 1) : 0;
SDL_Cursor* cursor = SDL_CreateColorCursor(surface, hot_x, hot_y);
SDL_DestroySurface(surface);
return cursor;
}
#endif
} // namespace
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
void VulkanWindow::enable_game_hardware_cursor() {
hardware_cursor_enabled_ = true;
for (int shape = 0; shape < static_cast<int>(game_cursors_.size()); ++shape) {
game_cursors_[shape] = load_game_cursor(shape);
}
fallback_cursor_ = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_DEFAULT);
SDL_SetCursor(game_cursors_[0] ? game_cursors_[0] : fallback_cursor_);
SDL_ShowCursor();
os_cursor_hidden_ = false;
}
void VulkanWindow::sync_game_cursor() {
if (!hardware_cursor_enabled_) return;
const bool visible = !touch_controller.is_enabled() && PythonBoot::CursorVisible() &&
!PythonBoot::IsSoftwareCursorVisible();
if (visible == os_cursor_hidden_) {
if (visible) SDL_ShowCursor();
else SDL_HideCursor();
os_cursor_hidden_ = !visible;
}
if (!visible) return;
const int shape = PythonBoot::CursorShape();
if (shape == current_cursor_shape_) return;
SDL_Cursor* cursor = shape >= 0 && shape < static_cast<int>(game_cursors_.size())
? game_cursors_[shape] : nullptr;
if (cursor || fallback_cursor_) SDL_SetCursor(cursor ? cursor : fallback_cursor_);
current_cursor_shape_ = shape;
}
#endif
void VulkanWindow::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()));
}
double VulkanWindow::input_idle_seconds() const {
if (fingers_down_ > 0 || buttons_down_ > 0) return 0.0;
return std::chrono::duration<double>(std::chrono::steady_clock::now() - last_input_).count();
}
void VulkanWindow::request_screenshot(std::string path, std::uint64_t frame) {
screenshot_path_ = std::move(path);
screenshot_frame_ = frame;
}
std::uint32_t VulkanWindow::logical_width() const {
int w = 0, h = 0;
if (window_) SDL_GetWindowSize(window_, &w, &h);
#ifdef __ANDROID__
// Keep the 40250 UI at the same height as the macOS 1280x800 window,
// while retaining the device aspect ratio on wider phone displays.
if (w > 0 && h > 0) return std::max(1, int(std::lround(double(w) * 800.0 / h)));
#endif
return w > 0 ? static_cast<std::uint32_t>(w) : extent_.width;
}
std::uint32_t VulkanWindow::logical_height() const {
int w = 0, h = 0;
if (window_) SDL_GetWindowSize(window_, &w, &h);
#ifdef __ANDROID__
if (w > 0 && h > 0) return 800;
#endif
return h > 0 ? static_cast<std::uint32_t>(h) : extent_.height;
}
int VulkanWindow::ui_safe_inset() const {
int pw = 0, ph = 0;
if (safe_inset_px_ <= 0 || !window_ || !SDL_GetWindowSizeInPixels(window_, &pw, &ph) || ph <= 0) return 0;
return int(std::lround(double(safe_inset_px_) * logical_height() / ph));
}
void VulkanWindow::sync_screen_keyboard() {
const bool want = touch_controller.wants_screen_keyboard();
const unsigned serial = touch_controller.screen_keyboard_serial();
if (want && (!SDL_TextInputActive(window_) || serial != screen_keyboard_serial_)) {
SDL_PropertiesID props = SDL_CreateProperties();
SDL_SetNumberProperty(props, SDL_PROP_TEXTINPUT_TYPE_NUMBER, SDL_TEXTINPUT_TYPE_TEXT);
SDL_SetNumberProperty(props, SDL_PROP_TEXTINPUT_CAPITALIZATION_NUMBER, SDL_CAPITALIZE_NONE);
SDL_SetBooleanProperty(props, SDL_PROP_TEXTINPUT_AUTOCORRECT_BOOLEAN, false);
SDL_StartTextInputWithProperties(window_, props);
SDL_DestroyProperties(props);
} else if (!want && SDL_TextInputActive(window_)) {
SDL_StopTextInput(window_);
}
screen_keyboard_serial_ = serial;
}
bool VulkanWindow::poll(bool forward_to_live_client) {
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
auto map_mouse = [&](float wx, float wy, int& ox, int& oy) {
int win_w = 0, win_h = 0;
SDL_GetWindowSize(window_, &win_w, &win_h);
unsigned ui_w = extent_.width ? extent_.width : 960;
unsigned ui_h = extent_.height ? extent_.height : 640;
UIRenderGetSize(&ui_w, &ui_h);
if (win_w > 0 && win_h > 0 && ui_w > 0 && ui_h > 0) {
ox = static_cast<int>(std::lround(wx * float(ui_w) / float(win_w)));
oy = static_cast<int>(std::lround(wy * float(ui_h) / float(win_h)));
} else {
ox = static_cast<int>(std::lround(wx));
oy = static_cast<int>(std::lround(wy));
}
};
std::optional<std::pair<int, int>> pending_mouse_move;
auto flush_mouse_move = [&] {
if (!pending_mouse_move) return;
const auto [mx, my] = *pending_mouse_move;
pending_mouse_move.reset();
last_mx_ = mx;
last_my_ = my;
if (touch_controller.is_enabled() && touch_controller.on_mouse_motion(mx, my)) return;
PythonBoot::UIMouseMove(mx, my);
};
#endif
SDL_Event event;
while (SDL_PollEvent(&event)) {
note_input(event);
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
// Touches arrive as SDL_EVENT_FINGER_*; the touch-synthesized mouse copy
// would register as a second finger (101) and turn every drag into a pinch.
if (touch_controller.is_enabled() &&
((event.type == SDL_EVENT_MOUSE_MOTION && event.motion.which == SDL_TOUCH_MOUSEID) ||
((event.type == SDL_EVENT_MOUSE_BUTTON_DOWN || event.type == SDL_EVENT_MOUSE_BUTTON_UP) &&
event.button.which == SDL_TOUCH_MOUSEID)))
continue;
if (forward_to_live_client && event.type == SDL_EVENT_MOUSE_MOTION) {
int mx = 0, my = 0;
map_mouse(event.motion.x, event.motion.y, mx, my);
pending_mouse_move = std::pair{mx, my};
continue;
}
// Preserve event order at button/key/focus boundaries while collapsing
// high-frequency motion into the most recent position.
flush_mouse_move();
#endif
if (event.type == SDL_EVENT_QUIT) return false;
if (event.type == SDL_EVENT_WINDOW_RESIZED || event.type == SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED) {
int ew = 0, eh = 0;
SDL_GetWindowSize(window_, &ew, &eh);
touch_controller.update_screen_size(int(logical_width()), int(logical_height()), ui_safe_inset());
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
if (forward_to_live_client && ew > 0 && eh > 0) {
PythonBoot::SetUISafeInset(ui_safe_inset());
PythonBoot::SetUISize(int(logical_width()), int(logical_height()));
}
#endif
recreate_swapchain();
continue;
}
if (event.type == SDL_EVENT_FINGER_DOWN) {
touch_controller.on_finger_down(event.tfinger.fingerID, event.tfinger.x, event.tfinger.y);
continue;
} else if (event.type == SDL_EVENT_FINGER_MOTION) {
touch_controller.on_finger_motion(event.tfinger.fingerID, event.tfinger.x, event.tfinger.y);
continue;
} else if (event.type == SDL_EVENT_FINGER_UP || event.type == SDL_EVENT_FINGER_CANCELED) {
touch_controller.on_finger_up(event.tfinger.fingerID, event.tfinger.x, event.tfinger.y);
continue;
}
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
if (forward_to_live_client) {
if (!hardware_cursor_enabled_ &&
(event.type == SDL_EVENT_WINDOW_MOUSE_ENTER || event.type == SDL_EVENT_WINDOW_FOCUS_GAINED)) {
std::printf(">>> SDL MOUSE ENTER/FOCUS GAINED\n");
SDL_HideCursor();
os_cursor_hidden_ = true;
} else if (event.type == SDL_EVENT_WINDOW_FOCUS_LOST) {
SDL_CaptureMouse(false);
PythonBoot::UIMouseButton(2, false, last_mx_, last_my_);
PythonBoot::UIMouseButton(3, false, last_mx_, last_my_);
PythonBoot::UIMouseButton(1, false, last_mx_, last_my_);
}
if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN || event.type == SDL_EVENT_MOUSE_BUTTON_UP) {
const bool pressed = event.type == SDL_EVENT_MOUSE_BUTTON_DOWN;
int btn = 0;
if (event.button.button == SDL_BUTTON_LEFT) btn = 1;
else if (event.button.button == SDL_BUTTON_RIGHT) btn = 2;
else if (event.button.button == SDL_BUTTON_MIDDLE) btn = 3;
if (btn) {
int mx = 0, my = 0;
map_mouse(event.button.x, event.button.y, mx, my);
last_mx_ = mx;
last_my_ = my;
if (touch_controller.is_enabled()) {
if (touch_controller.on_mouse_button(btn, pressed, mx, my)) {
continue;
}
}
SDL_CaptureMouse(SDL_GetMouseState(nullptr, nullptr) != 0);
PythonBoot::UIMouseButton(btn, pressed, mx, my);
}
} else if (event.type == SDL_EVENT_MOUSE_WHEEL) {
PythonBoot::UIMouseWheel(int(event.wheel.y * 120.0f));
} else if (event.type == SDL_EVENT_KEY_DOWN || event.type == SDL_EVENT_KEY_UP) {
const bool pressed = event.type == SDL_EVENT_KEY_DOWN;
// Android back = ESC: closes the top window, or opens the system menu (game.py).
if (event.key.scancode == SDL_SCANCODE_AC_BACK) event.key.scancode = SDL_SCANCODE_ESCAPE;
if (pressed) {
if (const int vk = sdl_scancode_to_vk(event.key.scancode))
PythonBoot::UIIMEKeyDown(vk);
if (event.key.scancode == SDL_SCANCODE_BACKSPACE) PythonBoot::UIChar(8);
else if (event.key.scancode == SDL_SCANCODE_TAB) PythonBoot::UIChar(9);
else if (event.key.scancode == SDL_SCANCODE_RETURN || event.key.scancode == SDL_SCANCODE_KP_ENTER)
PythonBoot::UIChar(13);
else if (event.key.scancode == SDL_SCANCODE_ESCAPE) PythonBoot::UIChar(27);
}
if (!event.key.repeat) {
if (const int dik = sdl_scancode_to_dik(event.key.scancode))
PythonBoot::UIKey(dik, pressed);
}
} else if (event.type == SDL_EVENT_TEXT_INPUT && event.text.text) {
const auto* s = reinterpret_cast<const unsigned char*>(event.text.text);
while (*s) {
unsigned cp = 0;
if (*s < 0x80u) {
cp = *s++;
} else if ((*s & 0xE0u) == 0xC0u && (s[1] & 0xC0u) == 0x80u) {
cp = ((*s & 0x1Fu) << 6) | (s[1] & 0x3Fu);
s += 2;
} else if ((*s & 0xF0u) == 0xE0u && (s[1] & 0xC0u) == 0x80u && (s[2] & 0xC0u) == 0x80u) {
cp = ((*s & 0x0Fu) << 12) | ((s[1] & 0x3Fu) << 6) | (s[2] & 0x3Fu);
s += 3;
} else if ((*s & 0xF8u) == 0xF0u && (s[1] & 0xC0u) == 0x80u && (s[2] & 0xC0u) == 0x80u && (s[3] & 0xC0u) == 0x80u) {
cp = ((*s & 0x07u) << 18) | ((s[1] & 0x3Fu) << 12) | ((s[2] & 0x3Fu) << 6) | (s[3] & 0x3Fu);
s += 4;
} else {
++s;
continue;
}
if (cp >= 32u && cp != 127u)
PythonBoot::UIChar(cp);
}
}
}
#else
(void)forward_to_live_client;
#endif
}
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
flush_mouse_move();
#endif
touch_controller.update();
if (touch_controller.is_enabled())
sync_screen_keyboard();
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
if (forward_to_live_client && !hardware_cursor_enabled_ && !os_cursor_hidden_ && !touch_controller.is_enabled()) {
SDL_HideCursor();
os_cursor_hidden_ = true;
}
#endif
return true;
}
void VulkanWindow::update_fps_counter() {
const auto now = std::chrono::steady_clock::now();
if (fps_frames_++ == 0) {
fps_window_start_ = now;
return;
}
const double elapsed = std::chrono::duration<double>(now - fps_window_start_).count();
if (elapsed >= 0.5) {
fps_ = int(std::lround((fps_frames_ - 1) / elapsed));
fps_frames_ = 1;
fps_window_start_ = now;
}
}
void VulkanWindow::note_input(const SDL_Event& event) {
switch (event.type) {
case SDL_EVENT_FINGER_DOWN: ++fingers_down_; break;
case SDL_EVENT_FINGER_UP:
case SDL_EVENT_FINGER_CANCELED: fingers_down_ = std::max(0, fingers_down_ - 1); break;
case SDL_EVENT_MOUSE_BUTTON_DOWN: ++buttons_down_; break;
case SDL_EVENT_MOUSE_BUTTON_UP: buttons_down_ = std::max(0, buttons_down_ - 1); break;
case SDL_EVENT_WINDOW_FOCUS_LOST: case SDL_EVENT_WILL_ENTER_BACKGROUND:
// An up event lost with the focus must not keep the client out of the idle rate for good.
fingers_down_ = buttons_down_ = 0;
break;
case SDL_EVENT_FINGER_MOTION: case SDL_EVENT_MOUSE_MOTION: case SDL_EVENT_MOUSE_WHEEL:
case SDL_EVENT_KEY_DOWN: case SDL_EVENT_KEY_UP: case SDL_EVENT_TEXT_INPUT:
case SDL_EVENT_WINDOW_FOCUS_GAINED: case SDL_EVENT_DID_ENTER_FOREGROUND: break;
default: return;
}
last_input_ = std::chrono::steady_clock::now();
}
} // namespace mt_host
+194
View File
@@ -0,0 +1,194 @@
// 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
+251
View File
@@ -0,0 +1,251 @@
// VulkanWindow: physical device selection, swapchain, MSAA target and framebuffers.
#include "vulkan_window.h"
#include "host_util.h"
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <stdexcept>
namespace mt_host {
std::uint32_t VulkanWindow::find_memory_type(std::uint32_t type_bits, VkMemoryPropertyFlags flags) const {
VkPhysicalDeviceMemoryProperties properties{};
vkGetPhysicalDeviceMemoryProperties(physical_, &properties);
for (std::uint32_t i = 0; i < properties.memoryTypeCount; ++i)
if ((type_bits & (1u << i)) && (properties.memoryTypes[i].propertyFlags & flags) == flags)
return i;
throw std::runtime_error("no matching Vulkan memory type");
}
void VulkanWindow::select_device() {
std::uint32_t count = 0;
check(vkEnumeratePhysicalDevices(instance_, &count, nullptr), "vkEnumeratePhysicalDevices count");
if (!count) throw std::runtime_error("no Vulkan physical device; set VK_ICD_FILENAMES for MoltenVK");
std::vector<VkPhysicalDevice> candidates(count);
check(vkEnumeratePhysicalDevices(instance_, &count, candidates.data()), "vkEnumeratePhysicalDevices");
for (auto candidate : candidates) {
std::uint32_t family_count = 0;
vkGetPhysicalDeviceQueueFamilyProperties(candidate, &family_count, nullptr);
std::vector<VkQueueFamilyProperties> families(family_count);
vkGetPhysicalDeviceQueueFamilyProperties(candidate, &family_count, families.data());
for (std::uint32_t i = 0; i < family_count; ++i) {
VkBool32 present = VK_FALSE;
check(vkGetPhysicalDeviceSurfaceSupportKHR(candidate, i, surface_, &present), "surface support");
if ((families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) && present) {
physical_ = candidate; queue_family_ = i; break;
}
}
if (physical_) break;
}
if (!physical_) throw std::runtime_error("no graphics/present queue family");
VkPhysicalDeviceProperties properties{};
vkGetPhysicalDeviceProperties(physical_, &properties);
device_name_ = properties.deviceName;
timestamp_period_ns_ = properties.limits.timestampPeriod;
VkPhysicalDeviceFeatures supported_features{};
vkGetPhysicalDeviceFeatures(physical_, &supported_features);
VkPhysicalDeviceFeatures enabled_features{};
if (supported_features.samplerAnisotropy) {
enabled_features.samplerAnisotropy = VK_TRUE;
max_anisotropy_ = std::min(16.0f, properties.limits.maxSamplerAnisotropy);
}
// Multisampled 3D edges; MT_MSAA=1 keeps the single-sample path, 2/4/8 request a count.
{
int wanted = 4;
if (const char* env = std::getenv("MT_MSAA")) wanted = std::max(1, std::atoi(env));
const VkSampleCountFlags supported =
properties.limits.framebufferColorSampleCounts & properties.limits.framebufferDepthSampleCounts;
samples_ = VK_SAMPLE_COUNT_1_BIT;
for (VkSampleCountFlagBits c : {VK_SAMPLE_COUNT_8_BIT, VK_SAMPLE_COUNT_4_BIT, VK_SAMPLE_COUNT_2_BIT})
if (int(c) <= wanted && (supported & c)) { samples_ = c; break; }
}
std::uint32_t extension_count = 0;
check(vkEnumerateDeviceExtensionProperties(physical_, nullptr, &extension_count, nullptr), "device extensions count");
std::vector<VkExtensionProperties> available(extension_count);
check(vkEnumerateDeviceExtensionProperties(physical_, nullptr, &extension_count, available.data()), "device extensions");
std::vector<const char*> extensions{VK_KHR_SWAPCHAIN_EXTENSION_NAME};
constexpr const char* portability_subset = "VK_KHR_portability_subset";
for (const auto& extension : available)
if (std::strcmp(extension.extensionName, portability_subset) == 0)
extensions.push_back(portability_subset);
const float priority = 1.0f;
VkDeviceQueueCreateInfo queue_info{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};
queue_info.queueFamilyIndex = queue_family_; queue_info.queueCount = 1; queue_info.pQueuePriorities = &priority;
VkDeviceCreateInfo device_info{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
device_info.queueCreateInfoCount = 1; device_info.pQueueCreateInfos = &queue_info;
device_info.enabledExtensionCount = static_cast<std::uint32_t>(extensions.size());
device_info.ppEnabledExtensionNames = extensions.data();
device_info.pEnabledFeatures = &enabled_features;
check(vkCreateDevice(physical_, &device_info, nullptr, &device_), "vkCreateDevice");
vkGetDeviceQueue(device_, queue_family_, 0, &queue_);
}
void VulkanWindow::create_swapchain() {
VkSurfaceCapabilitiesKHR capabilities{};
check(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physical_, surface_, &capabilities), "surface capabilities");
std::uint32_t count = 0;
check(vkGetPhysicalDeviceSurfaceFormatsKHR(physical_, surface_, &count, nullptr), "surface formats count");
if (!count) throw std::runtime_error("surface has no formats");
std::vector<VkSurfaceFormatKHR> formats(count);
check(vkGetPhysicalDeviceSurfaceFormatsKHR(physical_, surface_, &count, formats.data()), "surface formats");
VkSurfaceFormatKHR format = formats.front();
for (auto candidate : formats) if (candidate.format == VK_FORMAT_B8G8R8A8_UNORM) format = candidate;
swapchain_format_ = format.format;
int width = 0, height = 0;
SDL_GetWindowSizeInPixels(window_, &width, &height);
if (capabilities.currentExtent.width != UINT32_MAX) extent_ = capabilities.currentExtent;
else {
extent_.width = std::clamp(std::uint32_t(width), capabilities.minImageExtent.width, capabilities.maxImageExtent.width);
extent_.height = std::clamp(std::uint32_t(height), capabilities.minImageExtent.height, capabilities.maxImageExtent.height);
}
present_mode_ = VK_PRESENT_MODE_FIFO_KHR;
if (!vsync_) {
std::uint32_t mode_count = 0;
check(vkGetPhysicalDeviceSurfacePresentModesKHR(physical_, surface_, &mode_count, nullptr), "present modes count");
std::vector<VkPresentModeKHR> modes(mode_count);
if (mode_count)
check(vkGetPhysicalDeviceSurfacePresentModesKHR(physical_, surface_, &mode_count, modes.data()), "present modes");
if (std::find(modes.begin(), modes.end(), VK_PRESENT_MODE_IMMEDIATE_KHR) != modes.end())
present_mode_ = VK_PRESENT_MODE_IMMEDIATE_KHR;
else if (std::find(modes.begin(), modes.end(), VK_PRESENT_MODE_MAILBOX_KHR) != modes.end())
present_mode_ = VK_PRESENT_MODE_MAILBOX_KHR;
}
VkSwapchainCreateInfoKHR info{VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR};
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 |
(capabilities.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_SRC_BIT);
info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
// Android surfaces can report a quarter-turn transform even when SDL's window is
// already landscape. Rendering in SDL window coordinates and requesting that transform
// again rotates the entire game image on presentation.
info.preTransform = (capabilities.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR)
? VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR : capabilities.currentTransform;
#ifdef __ANDROID__
SDL_Log("swapchain: window=%dx%d extent=%ux%u currentTransform=%u preTransform=%u",
width, height, extent_.width, extent_.height,
unsigned(capabilities.currentTransform), unsigned(info.preTransform));
#endif
info.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; info.presentMode = present_mode_;
info.clipped = VK_TRUE;
check(vkCreateSwapchainKHR(device_, &info, nullptr, &swapchain_), "vkCreateSwapchainKHR");
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_;
view.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
view.subresourceRange.levelCount = 1; view.subresourceRange.layerCount = 1;
VkImageView handle = VK_NULL_HANDLE;
check(vkCreateImageView(device_, &view, nullptr, &handle), "vkCreateImageView");
image_views_.push_back(handle);
}
VkImageCreateInfo depth_info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
depth_info.imageType = VK_IMAGE_TYPE_2D;
depth_info.format = VK_FORMAT_D32_SFLOAT;
depth_info.extent = {extent_.width, extent_.height, 1};
depth_info.mipLevels = 1; depth_info.arrayLayers = 1;
depth_info.samples = samples_;
depth_info.tiling = VK_IMAGE_TILING_OPTIMAL;
depth_info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
depth_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
check(vkCreateImage(device_, &depth_info, nullptr, &depth_image_), "vkCreateImage depth");
VkMemoryRequirements depth_reqs{};
vkGetImageMemoryRequirements(device_, depth_image_, &depth_reqs);
VkMemoryAllocateInfo depth_alloc{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
depth_alloc.allocationSize = depth_reqs.size;
depth_alloc.memoryTypeIndex = find_memory_type(depth_reqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
check(vkAllocateMemory(device_, &depth_alloc, nullptr, &depth_memory_), "vkAllocateMemory depth");
check(vkBindImageMemory(device_, depth_image_, depth_memory_, 0), "vkBindImageMemory depth");
VkImageViewCreateInfo depth_view{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
depth_view.image = depth_image_; depth_view.viewType = VK_IMAGE_VIEW_TYPE_2D;
depth_view.format = VK_FORMAT_D32_SFLOAT;
depth_view.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
depth_view.subresourceRange.levelCount = 1; depth_view.subresourceRange.layerCount = 1;
check(vkCreateImageView(device_, &depth_view, nullptr, &depth_view_), "vkCreateImageView depth");
if (samples_ != VK_SAMPLE_COUNT_1_BIT) create_msaa_color();
}
void VulkanWindow::recreate_swapchain() {
int w = 0, h = 0;
SDL_GetWindowSizeInPixels(window_, &w, &h);
if (w <= 0 || h <= 0) return;
vkDeviceWaitIdle(device_);
for (auto& entry : pipelines_) vkDestroyPipeline(device_, entry.second, nullptr);
pipelines_.clear();
for (auto fb : framebuffers_) vkDestroyFramebuffer(device_, fb, nullptr);
framebuffers_.clear();
if (depth_view_) { vkDestroyImageView(device_, depth_view_, nullptr); depth_view_ = VK_NULL_HANDLE; }
if (depth_image_) { vkDestroyImage(device_, depth_image_, nullptr); depth_image_ = VK_NULL_HANDLE; }
if (depth_memory_) { vkFreeMemory(device_, depth_memory_, nullptr); depth_memory_ = VK_NULL_HANDLE; }
destroy_msaa_color();
for (auto view : image_views_) vkDestroyImageView(device_, view, nullptr);
image_views_.clear();
if (swapchain_) { vkDestroySwapchainKHR(device_, swapchain_, nullptr); swapchain_ = VK_NULL_HANDLE; }
create_swapchain();
create_framebuffers();
get_pipeline(0, 2, 0);
}
void VulkanWindow::create_msaa_color() {
VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
info.imageType = VK_IMAGE_TYPE_2D;
info.format = swapchain_format_;
info.extent = {extent_.width, extent_.height, 1};
info.mipLevels = 1; info.arrayLayers = 1;
info.samples = samples_;
info.tiling = VK_IMAGE_TILING_OPTIMAL;
info.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT;
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
check(vkCreateImage(device_, &info, nullptr, &msaa_image_), "vkCreateImage msaa color");
VkMemoryRequirements reqs{};
vkGetImageMemoryRequirements(device_, msaa_image_, &reqs);
VkMemoryAllocateInfo alloc{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
alloc.allocationSize = reqs.size;
try {
alloc.memoryTypeIndex = find_memory_type(reqs.memoryTypeBits, VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT);
} catch (const std::runtime_error&) {
alloc.memoryTypeIndex = find_memory_type(reqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
}
check(vkAllocateMemory(device_, &alloc, nullptr, &msaa_memory_), "vkAllocateMemory msaa color");
check(vkBindImageMemory(device_, msaa_image_, msaa_memory_, 0), "vkBindImageMemory msaa color");
VkImageViewCreateInfo view{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
view.image = msaa_image_; view.viewType = VK_IMAGE_VIEW_TYPE_2D; view.format = swapchain_format_;
view.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
view.subresourceRange.levelCount = 1; view.subresourceRange.layerCount = 1;
check(vkCreateImageView(device_, &view, nullptr, &msaa_view_), "vkCreateImageView msaa color");
}
void VulkanWindow::destroy_msaa_color() {
if (msaa_view_) { vkDestroyImageView(device_, msaa_view_, nullptr); msaa_view_ = VK_NULL_HANDLE; }
if (msaa_image_) { vkDestroyImage(device_, msaa_image_, nullptr); msaa_image_ = VK_NULL_HANDLE; }
if (msaa_memory_) { vkFreeMemory(device_, msaa_memory_, nullptr); msaa_memory_ = VK_NULL_HANDLE; }
}
void VulkanWindow::create_framebuffers() {
for (auto view : image_views_) {
// Single-sample: {swapchain, depth}. MSAA: {msaa colour, depth, swapchain resolve}.
VkImageView fb_attachments[3] = {view, depth_view_, VK_NULL_HANDLE};
std::uint32_t count = 2;
if (samples_ != VK_SAMPLE_COUNT_1_BIT) {
fb_attachments[0] = msaa_view_;
fb_attachments[2] = view;
count = 3;
}
VkFramebufferCreateInfo framebuffer{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
framebuffer.renderPass = pass_; framebuffer.attachmentCount = count; framebuffer.pAttachments = fb_attachments;
framebuffer.width = extent_.width; framebuffer.height = extent_.height; framebuffer.layers = 1;
VkFramebuffer handle = VK_NULL_HANDLE;
check(vkCreateFramebuffer(device_, &framebuffer, nullptr, &handle), "vkCreateFramebuffer");
framebuffers_.push_back(handle);
}
}
} // namespace mt_host
+550
View File
@@ -0,0 +1,550 @@
// VulkanWindow::render: one frame from the recorded D3D8 draws and UI commands to present.
#include "vulkan_window.h"
#include "host_util.h"
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <stdexcept>
#include <unordered_set>
namespace mt_host {
VkViewport VulkanWindow::full_viewport() const {
return {0, 0, float(extent_.width), float(extent_.height), 0, 1};
}
VkViewport VulkanWindow::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] + draw.ui_offset_x) * sx, draw.viewport[1] * sy, draw.viewport[2] * sx, draw.viewport[3] * sy,
draw.viewport_z[0], draw.viewport_z[1]};
}
void VulkanWindow::render(
const std::vector<Render3DDraw>& draws,
const std::unordered_map<std::string, std::vector<std::uint8_t>>& capture_textures,
std::uint32_t ui_width,
std::uint32_t ui_height,
const std::vector<UIRenderCommand>& ui_commands) {
if (extent_.width == 0 || extent_.height == 0) return;
const auto start = std::chrono::steady_clock::now();
f_ = &frames_[frame_slot_ % frames_in_flight_];
check(vkWaitForFences(device_, 1, &f_->fence, VK_TRUE, UINT64_MAX), "vkWaitForFences");
collect_pending_gpu_timestamp(*f_);
reset_staging(*f_);
const auto fenced = std::chrono::steady_clock::now();
prune_textures();
std::uint32_t image_index = 0;
const auto acquired = vkAcquireNextImageKHR(device_, swapchain_, UINT64_MAX, f_->acquire, VK_NULL_HANDLE, &image_index);
if (acquired == VK_ERROR_OUT_OF_DATE_KHR) {
recreate_swapchain();
return;
}
if (acquired != VK_SUCCESS && acquired != VK_SUBOPTIMAL_KHR) check(acquired, "vkAcquireNextImageKHR");
const auto synchronized = std::chrono::steady_clock::now();
++frame_number_;
++timed_frames_;
while (rendered_.size() < swapchain_images_.size()) {
VkSemaphoreCreateInfo semaphore_info{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
VkSemaphore semaphore = VK_NULL_HANDLE;
check(vkCreateSemaphore(device_, &semaphore_info, nullptr, &semaphore), "vkCreateSemaphore rendered");
rendered_.push_back(semaphore);
}
// Texture uploads of this frame are recorded ahead of the render pass (upload_texture).
check(vkResetCommandBuffer(f_->command, 0), "vkResetCommandBuffer");
VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
check(vkBeginCommandBuffer(f_->command, &begin), "vkBeginCommandBuffer");
vkCmdResetQueryPool(f_->command, query_pool_, f_->query_base, 2);
vkCmdWriteTimestamp(f_->command, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, query_pool_, f_->query_base);
f_->recording = true;
FrameDraws frame;
prepare_draws(draws, capture_textures, frame);
std::vector<UiBatch> ui_batches;
std::size_t ui_quad_count = 0;
prepare_ui(ui_width, ui_height, ui_commands, capture_textures, ui_batches, ui_quad_count);
write_draw_states(frame, ui_batches);
const auto prepared = std::chrono::steady_clock::now();
f_->recording = false;
check(vkResetFences(device_, 1, &f_->fence), "vkResetFences");
record_passes(image_index, frame, ui_batches);
Readback readback = record_readbacks(image_index);
vkCmdWriteTimestamp(f_->command, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, query_pool_, f_->query_base + 1);
check(vkEndCommandBuffer(f_->command), "vkEndCommandBuffer");
f_->has_pending_query = true;
const VkPipelineStageFlags stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO};
submit.waitSemaphoreCount = 1; submit.pWaitSemaphores = &f_->acquire; submit.pWaitDstStageMask = &stage;
submit.commandBufferCount = 1; submit.pCommandBuffers = &f_->command;
submit.signalSemaphoreCount = 1; submit.pSignalSemaphores = &rendered_[image_index];
check(vkQueueSubmit(queue_, 1, &submit, f_->fence), "vkQueueSubmit");
++frame_slot_;
finish_readbacks(readback);
const auto submitted = std::chrono::steady_clock::now();
VkPresentInfoKHR present{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR};
present.waitSemaphoreCount = 1; present.pWaitSemaphores = &rendered_[image_index];
present.swapchainCount = 1; present.pSwapchains = &swapchain_; present.pImageIndices = &image_index;
const auto presented = vkQueuePresentKHR(queue_, &present);
// An Android surface can continuously report SUBOPTIMAL while the app deliberately
// uses the identity transform. The image is still presentable; rebuilding every frame
// stalls rendering without changing that status.
if (presented == VK_ERROR_OUT_OF_DATE_KHR) {
recreate_swapchain();
} else if (presented != VK_SUCCESS && presented != VK_SUBOPTIMAL_KHR) {
check(presented, "vkQueuePresentKHR");
}
const auto presented_at = std::chrono::steady_clock::now();
const auto prepare_ms = std::chrono::duration<double, std::milli>(prepared - synchronized).count();
last_sync_ms_ = std::chrono::duration<double, std::milli>(synchronized - start).count();
timings_.sync_ms += last_sync_ms_;
timings_.fence_ms += std::chrono::duration<double, std::milli>(fenced - start).count();
timings_.prepare_ms += prepare_ms;
if (timed_frames_ > 1) timings_.steady_prepare_ms += prepare_ms;
timings_.submit_ms += std::chrono::duration<double, std::milli>(submitted - prepared).count();
timings_.present_ms += std::chrono::duration<double, std::milli>(presented_at - submitted).count();
last_draw_count_ = frame.prepared_draws.size();
last_skinned_draw_count_ = frame.skinned_draw_count;
last_ui_batch_count_ = ui_batches.size();
last_ui_quad_count_ = ui_quad_count;
last_vertex_count_ = frame.vertex_count;
last_index_count_ = frame.index_count;
}
void VulkanWindow::prepare_draws(const std::vector<Render3DDraw>& draws, const TextureBytes& capture_textures, FrameDraws& frame) {
auto& prepared_draws = frame.prepared_draws;
auto& offscreen_draws = frame.offscreen_draws;
prepared_draws.reserve(draws.size());
std::unordered_set<std::uint64_t> active_keys;
std::uint32_t bone_cursor = 0;
std::size_t required_bones = 0;
for (const auto& draw : draws) {
if (!draw.bone_matrices.empty()) {
if (draw.bone_matrices.size() % 16)
throw std::runtime_error("invalid bone palette length");
required_bones += draw.bone_matrices.size() / 16;
}
}
ensure_bone_capacity(required_bones);
for (const auto& draw : draws) {
RenderTarget* target = nullptr;
if (draw.render_target) {
target = get_render_target(
Render3DRenderTargetName(draw.render_target, draw.target_width, draw.target_height));
if (!target) continue;
}
if (draw.clear_flags && target) {
PreparedDraw clear{};
clear.target = target;
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};
clear.clear_rect = {{0, 0}, {target->width, target->height}};
offscreen_draws.push_back(clear);
continue;
}
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};
auto [it, inserted] = geometries_.try_emplace(id);
auto& geometry = it->second;
if (inserted) {
upload_geometry(draw, geometry);
} else if (geometry.vertex_count != draw.positions.size() / 3 || geometry.index_count != draw.indices.size()) {
throw std::runtime_error("geometry key/revision reused with a different size");
}
geometry.last_used_frame = frame_number_;
if (draw.geometry_key) active_keys.insert(draw.geometry_key);
float skin_offset_encoded = 0.0f;
if (!draw.bone_matrices.empty() && draw.bone_matrices.size() % 16 == 0 && f_->bone_mapped) {
const auto bone_count = static_cast<std::uint32_t>(draw.bone_matrices.size() / 16);
if (std::size_t(bone_cursor) + bone_count <= f_->bone_capacity) {
std::memcpy(
f_->bone_mapped + std::size_t(bone_cursor) * 16,
draw.bone_matrices.data(),
draw.bone_matrices.size() * sizeof(float));
skin_offset_encoded = float(bone_cursor + 1u);
bone_cursor += bone_count;
++frame.skinned_draw_count;
} else throw std::runtime_error("bone palette capacity exceeded");
}
const bool is_alpha = draw.alpha_blend != 0;
std::uint8_t cull = 0;
// D3D8 cull mode names describe the winding to discard. With the
// shader's Y flip, clockwise is Vulkan's front-facing winding.
if (draw.cull_mode == 2) cull = 2; // D3DCULL_CW: discard clockwise
else if (draw.cull_mode == 3) cull = 1; // D3DCULL_CCW: discard counter-clockwise
const std::uint8_t depth = draw.z_enable ? (draw.z_write ? 2 : 1) : 0;
std::uint8_t blend = 0;
if (is_alpha) {
if (draw.alpha_blend != 0 && draw.src_blend >= 1 && draw.src_blend <= 11 &&
draw.dest_blend >= 1 && draw.dest_blend <= 11) {
blend = static_cast<std::uint8_t>((draw.src_blend & 0xFu) | ((draw.dest_blend & 0xFu) << 4));
} else {
blend = 1;
}
}
const auto pipeline = get_pipeline(cull, depth, blend, draw.lines,
static_cast<std::uint8_t>(draw.z_func), target != nullptr);
// 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);
auto constants = make_push_constants(draw, skin_offset_encoded);
if (draw.pretransformed) {
const float screen_width = float(logical_width());
const float screen_height = float(logical_height());
// Screen pixels (y down) to D3D NDC (y up); the shader's Y flip then maps to Vulkan.
constants.mvp = {2.0f / screen_width, 0, 0, 0,
0, -2.0f / screen_height, 0, 0,
0, 0, 1, 0,
-1, 1, 0, 1};
}
PreparedDraw prepared_draw{};
prepared_draw.target = target;
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);
if (draw.pretransformed && draw.ui_offset_x != 0)
prepared_draw.viewport.x += draw.ui_offset_x * float(extent_.width) / float(logical_width());
if (target) {
// D3DVIEWPORT8 in the target's own pixels.
prepared_draw.viewport = draw.viewport[2] > 0 && draw.viewport[3] > 0
? VkViewport{draw.viewport[0], draw.viewport[1], draw.viewport[2], draw.viewport[3],
draw.viewport_z[0], draw.viewport_z[1]}
: VkViewport{0, 0, float(target->width), float(target->height), 0, 1};
offscreen_draws.push_back(prepared_draw);
} else prepared_draws.push_back(prepared_draw);
frame.vertex_count += geometry.vertex_count;
frame.index_count += geometry.index_count;
}
for (auto it = geometries_.begin(); it != geometries_.end();) {
const auto age = frame_number_ - it->second.last_used_frame;
const bool obsolete_revision = it->first.key && active_keys.contains(it->first.key);
if (age > 120 || (age > 1 && (it->first.key == 0 || obsolete_revision))) {
release_geometry(it->second);
it = geometries_.erase(it);
} else ++it;
}
}
void VulkanWindow::prepare_ui(std::uint32_t ui_width, std::uint32_t ui_height, const std::vector<UIRenderCommand>& ui_commands,
const TextureBytes& capture_textures, std::vector<UiBatch>& ui_batches, std::size_t& ui_quad_count) {
if (f_->ui_mapped) {
const uint32_t target_ui_w = ui_width ? ui_width : 960;
const uint32_t target_ui_h = ui_height ? ui_height : 640;
update_fps_counter();
if (touch_controller.is_enabled() || show_fps_) {
std::vector<UIRenderCommand> combined_ui = ui_commands;
if (touch_controller.is_enabled()) {
touch_controller.update_screen_size(int(target_ui_w), int(target_ui_h), ui_safe_inset());
touch_controller.append_ui_commands(combined_ui);
}
if (show_fps_ && fps_ >= 0) {
// Top left, inside the safe area and clear of 40250's corner icon.
const float inset = float(std::min<int>(ui_safe_inset(), int(target_ui_w) / 8));
TouchController::draw_segment_text(combined_ui, "FPS " + std::to_string(fps_),
inset + 40.0f, 8.0f, 12.0f, 0xFF40FF40);
}
if (!combined_ui.empty()) {
build_ui_batches(target_ui_w, target_ui_h, combined_ui, capture_textures, ui_batches, ui_quad_count);
}
} else if (!ui_commands.empty()) {
build_ui_batches(target_ui_w, target_ui_h, ui_commands, capture_textures, ui_batches, ui_quad_count);
}
}
}
void VulkanWindow::write_draw_states(FrameDraws& frame, std::vector<UiBatch>& ui_batches) {
ensure_state_capacity(frame.prepared_draws.size() + frame.offscreen_draws.size() + ui_batches.size());
std::size_t state_index = 0;
for (auto& draw : frame.offscreen_draws) {
if (!draw.geometry) continue;
draw.state_offset = static_cast<std::uint32_t>(state_index * state_stride_);
std::memcpy(f_->state_mapped + draw.state_offset, &draw.fixed_state, sizeof(FixedFunctionState));
++state_index;
}
last_offscreen_draw_count_ = frame.offscreen_draws.size();
for (auto& draw : frame.prepared_draws) {
if (!draw.geometry) continue;
draw.state_offset = static_cast<std::uint32_t>(state_index * state_stride_);
std::memcpy(f_->state_mapped + draw.state_offset, &draw.fixed_state, sizeof(FixedFunctionState));
++state_index;
}
const FixedFunctionState ui_state{};
for (auto& batch : ui_batches) {
batch.state_offset = static_cast<std::uint32_t>(state_index * state_stride_);
std::memcpy(f_->state_mapped + batch.state_offset, &ui_state, sizeof(FixedFunctionState));
++state_index;
}
}
void VulkanWindow::record_passes(std::uint32_t image_index, const FrameDraws& frame, const std::vector<UiBatch>& ui_batches) {
VkClearValue clears[3]{};
// 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};
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(f_->command, 0, 1, &viewport);
current_viewport = viewport;
};
// One prepared draw or Clear inside the current render pass.
auto record_prepared = [&](const PreparedDraw& prepared_draw) {
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(f_->command, attachment_count, attachments, 1, &rect);
return;
}
set_viewport(prepared_draw.viewport);
if (prepared_draw.pipeline != current_pipeline) {
vkCmdBindPipeline(f_->command, VK_PIPELINE_BIND_POINT_GRAPHICS, prepared_draw.pipeline);
current_pipeline = prepared_draw.pipeline;
}
if (prepared_draw.descriptor != current_descriptor) {
vkCmdBindDescriptorSets(
f_->command, VK_PIPELINE_BIND_POINT_GRAPHICS, layout_, 0, 1, &prepared_draw.descriptor, 0, nullptr);
current_descriptor = prepared_draw.descriptor;
}
vkCmdBindDescriptorSets(f_->command, VK_PIPELINE_BIND_POINT_GRAPHICS, layout_, 1, 1,
&f_->bone_descriptor_set, 1, &prepared_draw.state_offset);
const auto& geometry = *prepared_draw.geometry;
const VkDeviceSize vertex_offset = 0;
vkCmdBindVertexBuffers(f_->command, 0, 1, &geometry.buffer, &vertex_offset);
vkCmdBindIndexBuffer(f_->command, geometry.buffer, geometry.index_offset, VK_INDEX_TYPE_UINT32);
vkCmdPushConstants(
f_->command,
layout_,
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
0,
sizeof(prepared_draw.constants),
&prepared_draw.constants);
vkCmdDrawIndexed(f_->command, geometry.index_count, 1, 0, 0, 0);
};
// Render targets first used this frame (drawn or only sampled) start cleared.
for (auto& entry : render_targets_)
if (!entry.second.initialized) initialize_render_target(entry.second);
// Offscreen passes: one per run of draws into the same target.
for (std::size_t i = 0; i < frame.offscreen_draws.size();) {
RenderTarget* target = frame.offscreen_draws[i].target;
VkRenderPassBeginInfo offscreen_begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
offscreen_begin.renderPass = offscreen_pass_;
offscreen_begin.framebuffer = target->framebuffer;
offscreen_begin.renderArea.extent = {target->width, target->height};
vkCmdBeginRenderPass(f_->command, &offscreen_begin, VK_SUBPASS_CONTENTS_INLINE);
for (; i < frame.offscreen_draws.size() && frame.offscreen_draws[i].target == target; ++i)
record_prepared(frame.offscreen_draws[i]);
vkCmdEndRenderPass(f_->command);
}
VkRenderPassBeginInfo pass_begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
pass_begin.renderPass = pass_; pass_begin.framebuffer = framebuffers_.at(image_index);
pass_begin.renderArea.extent = extent_; pass_begin.clearValueCount = samples_ != VK_SAMPLE_COUNT_1_BIT ? 3 : 2; pass_begin.pClearValues = clears;
vkCmdBeginRenderPass(f_->command, &pass_begin, VK_SUBPASS_CONTENTS_INLINE);
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) {
const VkDeviceSize v_offset = 0;
vkCmdBindVertexBuffers(f_->command, 0, 1, &f_->ui_buffer, &v_offset);
vkCmdBindIndexBuffer(f_->command, f_->ui_buffer, f_->ui_vertex_bytes, VK_INDEX_TYPE_UINT32);
ui_bound = true;
}
if (batch.pipeline != current_pipeline) {
vkCmdBindPipeline(f_->command, VK_PIPELINE_BIND_POINT_GRAPHICS, batch.pipeline);
current_pipeline = batch.pipeline;
}
if (batch.descriptor != current_descriptor) {
vkCmdBindDescriptorSets(
f_->command, VK_PIPELINE_BIND_POINT_GRAPHICS, layout_, 0, 1, &batch.descriptor, 0, nullptr);
current_descriptor = batch.descriptor;
}
vkCmdBindDescriptorSets(f_->command, VK_PIPELINE_BIND_POINT_GRAPHICS, layout_, 1, 1,
&f_->bone_descriptor_set, 1, &batch.state_offset);
vkCmdPushConstants(
f_->command,
layout_,
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
0,
sizeof(batch.constants),
&batch.constants);
vkCmdDrawIndexed(f_->command, batch.index_count, 1, batch.first_index, 0, 0);
}
};
record_ui_pass(true);
for (const auto& prepared_draw : frame.prepared_draws) record_prepared(prepared_draw);
record_ui_pass(false);
vkCmdEndRenderPass(f_->command);
}
VulkanWindow::Readback VulkanWindow::record_readbacks(std::uint32_t image_index) {
Readback readback;
readback.screenshot = !screenshot_path_.empty() && frame_number_ == screenshot_frame_;
if (readback.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.buffer), "vkCreateBuffer readback");
VkMemoryRequirements requirements{};
vkGetBufferMemoryRequirements(device_, readback.buffer, &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.buffer, 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(f_->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(f_->command, barrier.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.buffer, 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(f_->command, VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
}
// Debug: MT_DUMP_RT=prefix writes each render target of the screenshot frame to <prefix>_<n>.pgm.
const char* dump_rt = std::getenv("MT_DUMP_RT");
if (readback.screenshot && dump_rt && *dump_rt) {
readback.dump_rt = dump_rt;
const std::uint32_t texel = offscreen_format_ == VK_FORMAT_R5G6B5_UNORM_PACK16 ? 2 : 4;
for (auto& entry : render_targets_) {
RenderTarget& rt = entry.second;
Readback::RtDump dump{VK_NULL_HANDLE, VK_NULL_HANDLE, rt.width, rt.height};
VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
info.size = VkDeviceSize(rt.width) * rt.height * texel; info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
check(vkCreateBuffer(device_, &info, nullptr, &dump.buffer), "vkCreateBuffer rt dump");
VkMemoryRequirements requirements{};
vkGetBufferMemoryRequirements(device_, dump.buffer, &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, &dump.memory), "vkAllocateMemory rt dump");
check(vkBindBufferMemory(device_, dump.buffer, dump.memory, 0), "vkBindBufferMemory rt dump");
VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
barrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT;
barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
barrier.oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = rt.texture.image;
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
vkCmdPipelineBarrier(f_->command, VK_PIPELINE_STAGE_ALL_COMMANDS_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 = {rt.width, rt.height, 1};
vkCmdCopyImageToBuffer(f_->command, rt.texture.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dump.buffer, 1, &region);
barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
vkCmdPipelineBarrier(f_->command, VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
readback.rt_dumps.push_back(dump);
}
}
return readback;
}
void VulkanWindow::finish_readbacks(Readback& readback) {
if (!readback.screenshot) return;
check(vkWaitForFences(device_, 1, &f_->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);
const std::uint32_t texel = offscreen_format_ == VK_FORMAT_R5G6B5_UNORM_PACK16 ? 2 : 4;
for (std::size_t n = 0; n < readback.rt_dumps.size(); ++n) {
const auto& dump = readback.rt_dumps[n];
void* data = nullptr;
check(vkMapMemory(device_, dump.memory, 0, VK_WHOLE_SIZE, 0, &data), "vkMapMemory rt dump");
const auto* bytes = static_cast<const std::uint8_t*>(data);
std::ofstream out(std::string(readback.dump_rt) + "_" + std::to_string(n) + ".pgm", std::ios::binary);
out << "P5\n" << dump.w << " " << dump.h << "\n255\n";
for (std::size_t i = 0; i < std::size_t(dump.w) * dump.h; ++i) {
std::uint8_t g = texel == 2 ? std::uint8_t(((bytes[i * 2 + 1] >> 3) & 31) * 255 / 31) : bytes[i * 4];
out.put(char(g));
}
vkUnmapMemory(device_, dump.memory);
vkDestroyBuffer(device_, dump.buffer, nullptr);
vkFreeMemory(device_, dump.memory, nullptr);
}
vkDestroyBuffer(device_, readback.buffer, nullptr);
vkFreeMemory(device_, readback.memory, nullptr);
}
} // namespace mt_host
+277
View File
@@ -0,0 +1,277 @@
// VulkanWindow: render passes, pipeline layout, shader modules and the pipeline cache keyed by D3D8 state.
#include "vulkan_window.h"
#include "host_util.h"
#include <cstring>
#include <stdexcept>
namespace mt_host {
namespace {
std::vector<std::uint32_t> read_spirv(const char* name) {
std::size_t size = 0;
void* bytes = SDL_LoadFile(bundled_file_path(name).c_str(), &size);
#ifdef __ANDROID__
if (!bytes) bytes = SDL_LoadFile((std::string("assets://") + name).c_str(), &size);
if (!bytes) bytes = SDL_LoadFile(name, &size);
#endif
if (!bytes) throw std::runtime_error(std::string("cannot open bundled shader: ") + name);
if (size == 0 || size % 4) {
SDL_free(bytes);
throw std::runtime_error(std::string("invalid SPIR-V size: ") + name);
}
std::vector<std::uint32_t> words(size / 4);
std::memcpy(words.data(), bytes, size);
SDL_free(bytes);
return words;
}
} // namespace
void VulkanWindow::create_render_pass_and_layout() {
const bool msaa = samples_ != VK_SAMPLE_COUNT_1_BIT;
VkAttachmentDescription attachments[3]{};
attachments[0].format = swapchain_format_; attachments[0].samples = samples_;
attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
attachments[0].storeOp = msaa ? VK_ATTACHMENT_STORE_OP_DONT_CARE : VK_ATTACHMENT_STORE_OP_STORE;
attachments[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
attachments[0].finalLayout = msaa ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
// MSAA resolve target: the swapchain image.
attachments[2].format = swapchain_format_; attachments[2].samples = VK_SAMPLE_COUNT_1_BIT;
attachments[2].loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; attachments[2].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachments[2].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; attachments[2].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
attachments[1].format = VK_FORMAT_D32_SFLOAT; attachments[1].samples = samples_;
attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachments[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
attachments[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
VkAttachmentReference color_ref{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
VkAttachmentReference depth_ref{1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL};
VkAttachmentReference resolve_ref{2, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &color_ref;
if (msaa) subpass.pResolveAttachments = &resolve_ref;
subpass.pDepthStencilAttachment = &depth_ref;
VkSubpassDependency dependency{};
dependency.srcSubpass = VK_SUBPASS_EXTERNAL; dependency.dstSubpass = 0;
dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
VkRenderPassCreateInfo pass_info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
pass_info.attachmentCount = msaa ? 3 : 2; pass_info.pAttachments = attachments;
pass_info.subpassCount = 1; pass_info.pSubpasses = &subpass;
pass_info.dependencyCount = 1; pass_info.pDependencies = &dependency;
check(vkCreateRenderPass(device_, &pass_info, nullptr, &pass_), "vkCreateRenderPass");
create_framebuffers();
const auto vert = read_spirv("native.vert.spv");
const auto frag = read_spirv("native.frag.spv");
auto module = [&](const std::vector<std::uint32_t>& code) {
VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
info.codeSize = code.size() * sizeof(std::uint32_t); info.pCode = code.data();
VkShaderModule handle = VK_NULL_HANDLE;
check(vkCreateShaderModule(device_, &info, nullptr, &handle), "vkCreateShaderModule");
return handle;
};
vertex_module_ = module(vert);
fragment_module_ = module(frag);
VkDescriptorSetLayout set_layouts[2] = {descriptor_layout_, bone_descriptor_layout_};
VkPushConstantRange push_constants{};
push_constants.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
push_constants.size = sizeof(PushConstants);
VkPipelineLayoutCreateInfo layout_info{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
layout_info.setLayoutCount = 2; layout_info.pSetLayouts = set_layouts;
layout_info.pushConstantRangeCount = 1; layout_info.pPushConstantRanges = &push_constants;
check(vkCreatePipelineLayout(device_, &layout_info, nullptr, &layout_), "vkCreatePipelineLayout");
get_pipeline(0, 2, 0);
create_offscreen_pass();
}
void VulkanWindow::create_offscreen_pass() {
VkFormatProperties properties{};
vkGetPhysicalDeviceFormatProperties(physical_, VK_FORMAT_R5G6B5_UNORM_PACK16, &properties);
const VkFormatFeatureFlags wanted =
VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT;
offscreen_format_ = (properties.optimalTilingFeatures & wanted) == wanted
? VK_FORMAT_R5G6B5_UNORM_PACK16 : VK_FORMAT_R8G8B8A8_UNORM;
VkAttachmentDescription attachments[2]{};
attachments[0].format = offscreen_format_; attachments[0].samples = VK_SAMPLE_COUNT_1_BIT;
attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachments[0].initialLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
attachments[0].finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
attachments[1].format = VK_FORMAT_D32_SFLOAT; attachments[1].samples = VK_SAMPLE_COUNT_1_BIT;
attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachments[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachments[1].initialLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
attachments[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
VkAttachmentReference color_ref{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
VkAttachmentReference depth_ref{1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL};
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &color_ref;
subpass.pDepthStencilAttachment = &depth_ref;
VkSubpassDependency dependencies[2]{};
// Earlier work on the queue -- the previous frame sampling the texture, the previous
// offscreen pass, the first-use clear -- completes before this pass writes it.
dependencies[0].srcSubpass = VK_SUBPASS_EXTERNAL; dependencies[0].dstSubpass = 0;
dependencies[0].srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT |
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT;
dependencies[0].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
dependencies[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT |
VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
dependencies[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
// The back-buffer pass samples the result.
dependencies[1].srcSubpass = 0; dependencies[1].dstSubpass = VK_SUBPASS_EXTERNAL;
dependencies[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependencies[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
dependencies[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
dependencies[1].dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
VkRenderPassCreateInfo pass_info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
pass_info.attachmentCount = 2; pass_info.pAttachments = attachments;
pass_info.subpassCount = 1; pass_info.pSubpasses = &subpass;
pass_info.dependencyCount = 2; pass_info.pDependencies = dependencies;
check(vkCreateRenderPass(device_, &pass_info, nullptr, &offscreen_pass_), "vkCreateRenderPass offscreen");
}
VkPipeline VulkanWindow::get_pipeline(std::uint8_t cull, std::uint8_t depth, std::uint8_t blend,
bool lines, std::uint8_t z_func, bool offscreen) {
const std::uint32_t key = std::uint32_t(cull) | (std::uint32_t(depth) << 8) |
(std::uint32_t(blend) << 16) | (lines ? (1u << 24) : 0u) |
(std::uint32_t(z_func & 0xfu) << 25) | (offscreen ? (1u << 29) : 0u);
if (auto it = pipelines_.find(key); it != pipelines_.end()) return it->second;
VkPipelineShaderStageCreateInfo stages[2]{};
stages[0].sType = stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vertex_module_; stages[0].pName = "main";
stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fragment_module_; stages[1].pName = "main";
VkVertexInputBindingDescription binding{0, sizeof(Vertex), VK_VERTEX_INPUT_RATE_VERTEX};
VkVertexInputAttributeDescription attributes[9] = {
{0, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(Vertex, position)},
{1, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(Vertex, normal)},
{2, 0, VK_FORMAT_R32G32_SFLOAT, offsetof(Vertex, uv)},
{3, 0, VK_FORMAT_R32G32B32A32_SFLOAT, offsetof(Vertex, color)},
{4, 0, VK_FORMAT_R8G8B8A8_UINT, offsetof(Vertex, joints)},
{5, 0, VK_FORMAT_R32G32B32A32_SFLOAT, offsetof(Vertex, weights)},
{6, 0, VK_FORMAT_R32G32_SFLOAT, offsetof(Vertex, mask_uv)},
{7, 0, VK_FORMAT_R32_SFLOAT, offsetof(Vertex, rhw)},
{8, 0, VK_FORMAT_R32_SFLOAT, offsetof(Vertex, vertex_fog)}};
VkPipelineVertexInputStateCreateInfo input{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
input.vertexBindingDescriptionCount = 1; input.pVertexBindingDescriptions = &binding;
input.vertexAttributeDescriptionCount = 9; input.pVertexAttributeDescriptions = attributes;
VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
assembly.topology = lines ? VK_PRIMITIVE_TOPOLOGY_LINE_LIST : VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
VkViewport viewport{0, 0, float(extent_.width), float(extent_.height), 0, 1};
// An offscreen target is at most the device's 4096 (GetDeviceCaps); the viewport clips inside it.
VkRect2D scissor{{0, 0}, offscreen ? VkExtent2D{4096, 4096} : extent_};
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;
raster.cullMode = cull == 1 ? VK_CULL_MODE_BACK_BIT : (cull == 2 ? VK_CULL_MODE_FRONT_BIT : VK_CULL_MODE_NONE);
raster.frontFace = VK_FRONT_FACE_CLOCKWISE;
raster.lineWidth = 1;
VkPipelineMultisampleStateCreateInfo multisample{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
multisample.rasterizationSamples = offscreen ? VK_SAMPLE_COUNT_1_BIT : samples_;
VkPipelineDepthStencilStateCreateInfo depth_stencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
depth_stencil.depthTestEnable = depth > 0 ? VK_TRUE : VK_FALSE;
depth_stencil.depthWriteEnable = depth > 1 ? VK_TRUE : VK_FALSE;
switch (z_func) {
case 1: depth_stencil.depthCompareOp = VK_COMPARE_OP_NEVER; break;
case 2: depth_stencil.depthCompareOp = VK_COMPARE_OP_LESS; break;
case 3: depth_stencil.depthCompareOp = VK_COMPARE_OP_EQUAL; break;
case 5: depth_stencil.depthCompareOp = VK_COMPARE_OP_GREATER; break;
case 6: depth_stencil.depthCompareOp = VK_COMPARE_OP_NOT_EQUAL; break;
case 7: depth_stencil.depthCompareOp = VK_COMPARE_OP_GREATER_OR_EQUAL; break;
case 8: depth_stencil.depthCompareOp = VK_COMPARE_OP_ALWAYS; break;
default: depth_stencil.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL; break;
}
auto d3d_to_vk_blend = [](std::uint8_t d3d, VkBlendFactor fallback) -> VkBlendFactor {
switch (d3d) {
case 1: return VK_BLEND_FACTOR_ZERO;
case 2: return VK_BLEND_FACTOR_ONE;
case 3: return VK_BLEND_FACTOR_SRC_COLOR;
case 4: return VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR;
case 5: return VK_BLEND_FACTOR_SRC_ALPHA;
case 6: return VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
case 7: return VK_BLEND_FACTOR_DST_ALPHA;
case 8: return VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA;
case 9: return VK_BLEND_FACTOR_DST_COLOR;
case 10: return VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR;
case 11: return VK_BLEND_FACTOR_SRC_ALPHA_SATURATE;
default: return fallback;
}
};
VkPipelineColorBlendAttachmentState blend_attachment{};
blend_attachment.colorWriteMask = 0xf;
if (blend > 0) {
blend_attachment.blendEnable = VK_TRUE;
if (blend >= 16) {
blend_attachment.srcColorBlendFactor =
d3d_to_vk_blend(blend & 0xFu, VK_BLEND_FACTOR_SRC_ALPHA);
blend_attachment.dstColorBlendFactor =
d3d_to_vk_blend((blend >> 4) & 0xFu, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA);
} else {
blend_attachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
blend_attachment.dstColorBlendFactor =
blend == 2 ? VK_BLEND_FACTOR_ONE : VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
}
blend_attachment.colorBlendOp = VK_BLEND_OP_ADD;
// 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};
blending.attachmentCount = 1; blending.pAttachments = &blend_attachment;
VkGraphicsPipelineCreateInfo pipeline_info{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
pipeline_info.stageCount = 2; pipeline_info.pStages = stages;
pipeline_info.pVertexInputState = &input; pipeline_info.pInputAssemblyState = &assembly;
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 = offscreen ? offscreen_pass_ : pass_;
VkPipeline handle = VK_NULL_HANDLE;
check(vkCreateGraphicsPipelines(device_, VK_NULL_HANDLE, 1, &pipeline_info, nullptr, &handle), "vkCreateGraphicsPipelines");
pipelines_.emplace(key, handle);
return handle;
}
} // namespace mt_host
+453
View File
@@ -0,0 +1,453 @@
// VulkanWindow: per-frame host buffers (bones, fixed-function state, UI, staging), descriptor pool, offscreen render targets and geometry buffers.
#include "vulkan_window.h"
#include "host_util.h"
#include <algorithm>
#include <cstring>
#include <stdexcept>
namespace mt_host {
void VulkanWindow::create_host_buffer(VkDeviceSize size, VkBufferUsageFlags usage, VkBuffer& buffer, VkDeviceMemory& memory, void** mapped) {
VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
info.size = size;
info.usage = usage;
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
check(vkCreateBuffer(device_, &info, nullptr, &buffer), "vkCreateBuffer host");
VkMemoryRequirements reqs{};
vkGetBufferMemoryRequirements(device_, buffer, &reqs);
VkMemoryAllocateInfo alloc{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
alloc.allocationSize = reqs.size;
alloc.memoryTypeIndex = find_memory_type(
reqs.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
check(vkAllocateMemory(device_, &alloc, nullptr, &memory), "vkAllocateMemory host");
check(vkBindBufferMemory(device_, buffer, memory, 0), "vkBindBufferMemory host");
check(vkMapMemory(device_, memory, 0, size, 0, mapped), "vkMapMemory host");
}
void VulkanWindow::create_descriptors_and_buffers() {
VkSamplerCreateInfo sampler_info{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
sampler_info.magFilter = VK_FILTER_LINEAR;
sampler_info.minFilter = VK_FILTER_LINEAR;
sampler_info.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
sampler_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
sampler_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
sampler_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
sampler_info.minLod = 0.0f;
sampler_info.maxLod = VK_LOD_CLAMP_NONE;
if (max_anisotropy_ > 1.0f) {
sampler_info.anisotropyEnable = VK_TRUE;
sampler_info.maxAnisotropy = max_anisotropy_;
}
check(vkCreateSampler(device_, &sampler_info, nullptr, &sampler_), "vkCreateSampler");
VkSamplerCreateInfo clamp_info = sampler_info;
clamp_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
clamp_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
clamp_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
check(vkCreateSampler(device_, &clamp_info, nullptr, &clamp_sampler_), "vkCreateSampler clamp");
VkSamplerCreateInfo ui_sampler_info{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
ui_sampler_info.magFilter = VK_FILTER_LINEAR;
ui_sampler_info.minFilter = VK_FILTER_LINEAR;
ui_sampler_info.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
ui_sampler_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
ui_sampler_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
ui_sampler_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
ui_sampler_info.minLod = 0.0f;
ui_sampler_info.maxLod = 0.0f;
ui_sampler_info.anisotropyEnable = VK_FALSE;
ui_sampler_info.maxAnisotropy = 1.0f;
check(vkCreateSampler(device_, &ui_sampler_info, nullptr, &ui_sampler_), "vkCreateSampler ui");
VkDescriptorSetLayoutBinding tex_bindings[2]{};
tex_bindings[0].binding = 0;
tex_bindings[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
tex_bindings[0].descriptorCount = 1;
tex_bindings[0].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
tex_bindings[1].binding = 1;
tex_bindings[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
tex_bindings[1].descriptorCount = 1;
tex_bindings[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
VkDescriptorSetLayoutCreateInfo layout_info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
layout_info.bindingCount = 2;
layout_info.pBindings = tex_bindings;
check(vkCreateDescriptorSetLayout(device_, &layout_info, nullptr, &descriptor_layout_), "vkCreateDescriptorSetLayout tex");
VkDescriptorSetLayoutBinding bone_bindings[2]{};
bone_bindings[0].binding = 0;
bone_bindings[0].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
bone_bindings[0].descriptorCount = 1;
bone_bindings[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
bone_bindings[1].binding = 1;
bone_bindings[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC;
bone_bindings[1].descriptorCount = 1;
bone_bindings[1].stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
VkDescriptorSetLayoutCreateInfo bone_layout_info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
bone_layout_info.bindingCount = 2;
bone_layout_info.pBindings = bone_bindings;
check(vkCreateDescriptorSetLayout(device_, &bone_layout_info, nullptr, &bone_descriptor_layout_), "vkCreateDescriptorSetLayout bone");
VkDescriptorPoolSize pool_sizes[3] = {
{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 16384},
{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4 * kMaxFramesInFlight},
{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, 4 * kMaxFramesInFlight}};
VkDescriptorPoolCreateInfo pool_info{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
pool_info.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
pool_info.maxSets = 8192;
pool_info.poolSizeCount = 3;
pool_info.pPoolSizes = pool_sizes;
check(vkCreateDescriptorPool(device_, &pool_info, nullptr, &descriptor_pool_), "vkCreateDescriptorPool");
const std::uint8_t white_pixel[4] = {255, 255, 255, 255};
upload_texture(1, 1, white_pixel, fallback_texture_, false, false);
fallback_texture_.descriptor = allocate_texture_descriptor_set(fallback_texture_.view, fallback_texture_.view);
fallback_texture_.ui_descriptor = allocate_ui_texture_descriptor_set(fallback_texture_.view, fallback_texture_.view);
VkPhysicalDeviceProperties properties{};
vkGetPhysicalDeviceProperties(physical_, &properties);
const auto alignment = std::max<VkDeviceSize>(
1, properties.limits.minStorageBufferOffsetAlignment);
state_stride_ = ((sizeof(FixedFunctionState) + alignment - 1) / alignment) * alignment;
staging_alignment_ = std::max<VkDeviceSize>(16, properties.limits.optimalBufferCopyOffsetAlignment);
for (auto& slot : frames_) {
void* bone_raw = nullptr;
create_host_buffer(kBoneBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, slot.bone_buffer, slot.bone_memory, &bone_raw);
slot.bone_mapped = static_cast<float*>(bone_raw);
slot.bone_capacity = kMaxBonesPerFrame;
for (std::size_t i = 0; i < 16; ++i) slot.bone_mapped[i] = kIdentityMatrix[i];
slot.state_capacity = 256;
void* state_raw = nullptr;
create_host_buffer(slot.state_capacity * state_stride_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT,
slot.state_buffer, slot.state_memory, &state_raw);
slot.state_mapped = static_cast<std::uint8_t*>(state_raw);
VkDescriptorSetAllocateInfo bone_alloc{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
bone_alloc.descriptorPool = descriptor_pool_;
bone_alloc.descriptorSetCount = 1;
bone_alloc.pSetLayouts = &bone_descriptor_layout_;
check(vkAllocateDescriptorSets(device_, &bone_alloc, &slot.bone_descriptor_set), "vkAllocateDescriptorSets bone");
VkDescriptorBufferInfo buffer_infos[2] = {
{slot.bone_buffer, 0, kBoneBufferBytes},
{slot.state_buffer, 0, sizeof(FixedFunctionState)}};
VkWriteDescriptorSet writes[2]{};
for (int binding = 0; binding < 2; ++binding) {
writes[binding].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writes[binding].dstSet = slot.bone_descriptor_set;
writes[binding].dstBinding = static_cast<std::uint32_t>(binding);
writes[binding].descriptorCount = 1;
writes[binding].descriptorType = binding == 0
? VK_DESCRIPTOR_TYPE_STORAGE_BUFFER : VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC;
writes[binding].pBufferInfo = &buffer_infos[binding];
}
vkUpdateDescriptorSets(device_, 2, writes, 0, nullptr);
void* ui_raw = nullptr;
create_host_buffer(
kUiBufferBytes,
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
slot.ui_buffer,
slot.ui_memory,
&ui_raw);
slot.ui_mapped = static_cast<std::uint8_t*>(ui_raw);
slot.ui_vertex_capacity = kMaxUiVerticesPerFrame;
slot.ui_index_capacity = kMaxUiIndicesPerFrame;
slot.ui_vertex_bytes = kUiVertexBytes;
slot.staging_wanted = kStagingRingBytes;
}
}
void VulkanWindow::release_retired_buffers(FrameSlot& slot) {
for (auto [buffer, memory] : slot.retired_buffers) {
vkDestroyBuffer(device_, buffer, nullptr);
vkFreeMemory(device_, memory, nullptr);
}
slot.retired_buffers.clear();
}
void VulkanWindow::reset_staging(FrameSlot& slot) {
release_retired_buffers(slot);
slot.staging_used = 0;
if (slot.staging_wanted <= slot.staging_capacity) return;
if (slot.staging_mapped) vkUnmapMemory(device_, slot.staging_memory);
if (slot.staging_buffer) vkDestroyBuffer(device_, slot.staging_buffer, nullptr);
if (slot.staging_memory) vkFreeMemory(device_, slot.staging_memory, nullptr);
void* raw = nullptr;
slot.staging_capacity = std::min(slot.staging_wanted, kStagingRingMaxBytes);
create_host_buffer(slot.staging_capacity, VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
slot.staging_buffer, slot.staging_memory, &raw);
slot.staging_mapped = static_cast<std::uint8_t*>(raw);
slot.staging_wanted = slot.staging_capacity;
}
void VulkanWindow::ensure_bone_capacity(std::size_t required) {
if (required <= f_->bone_capacity) return;
VkPhysicalDeviceProperties properties{};
vkGetPhysicalDeviceProperties(physical_, &properties);
const std::size_t max_bones = properties.limits.maxStorageBufferRange / (16 * sizeof(float));
if (required > max_bones || required > UINT32_MAX)
throw std::runtime_error("bone palette exceeds device storage-buffer limit");
std::size_t next = std::min(max_bones, std::max(required, f_->bone_capacity * 2));
VkBuffer buffer = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
void* mapped = nullptr;
create_host_buffer(next * 16 * sizeof(float), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, buffer, memory, &mapped);
vkUnmapMemory(device_, f_->bone_memory);
vkDestroyBuffer(device_, f_->bone_buffer, nullptr);
vkFreeMemory(device_, f_->bone_memory, nullptr);
f_->bone_buffer = buffer;
f_->bone_memory = memory;
f_->bone_mapped = static_cast<float*>(mapped);
f_->bone_capacity = next;
VkDescriptorBufferInfo info{f_->bone_buffer, 0, next * 16 * sizeof(float)};
VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
write.dstSet = f_->bone_descriptor_set;
write.dstBinding = 0;
write.descriptorCount = 1;
write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
write.pBufferInfo = &info;
vkUpdateDescriptorSets(device_, 1, &write, 0, nullptr);
}
void VulkanWindow::ensure_state_capacity(std::size_t required) {
if (required <= f_->state_capacity) return;
const auto next = std::max(required, f_->state_capacity * 2);
if (next > UINT32_MAX / state_stride_)
throw std::runtime_error("fixed-function state buffer exceeds dynamic offset range");
VkBuffer buffer = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
void* mapped = nullptr;
create_host_buffer(next * state_stride_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT,
buffer, memory, &mapped);
vkUnmapMemory(device_, f_->state_memory);
vkDestroyBuffer(device_, f_->state_buffer, nullptr);
vkFreeMemory(device_, f_->state_memory, nullptr);
f_->state_buffer = buffer;
f_->state_memory = memory;
f_->state_mapped = static_cast<std::uint8_t*>(mapped);
f_->state_capacity = next;
VkDescriptorBufferInfo info{f_->state_buffer, 0, sizeof(FixedFunctionState)};
VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
write.dstSet = f_->bone_descriptor_set;
write.dstBinding = 1;
write.descriptorCount = 1;
write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC;
write.pBufferInfo = &info;
vkUpdateDescriptorSets(device_, 1, &write, 0, nullptr);
}
void VulkanWindow::ensure_ui_capacity(std::size_t command_count) {
if (command_count <= f_->ui_vertex_capacity / 4 && command_count <= f_->ui_index_capacity / 6) return;
if (command_count > 100'000) throw std::runtime_error("UI command count exceeds safety limit");
const std::size_t quads = std::max(command_count, f_->ui_vertex_capacity / 2);
const VkDeviceSize vertex_bytes = VkDeviceSize(quads) * 4 * sizeof(Vertex);
const VkDeviceSize index_bytes = VkDeviceSize(quads) * 6 * sizeof(std::uint32_t);
if (vertex_bytes + index_bytes > 128ull * 1024ull * 1024ull)
throw std::runtime_error("UI geometry exceeds 128 MiB safety limit");
VkBuffer buffer = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
void* mapped = nullptr;
create_host_buffer(vertex_bytes + index_bytes,
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT, buffer, memory, &mapped);
vkUnmapMemory(device_, f_->ui_memory);
vkDestroyBuffer(device_, f_->ui_buffer, nullptr);
vkFreeMemory(device_, f_->ui_memory, nullptr);
f_->ui_buffer = buffer;
f_->ui_memory = memory;
f_->ui_mapped = static_cast<std::uint8_t*>(mapped);
f_->ui_vertex_capacity = quads * 4;
f_->ui_index_capacity = quads * 6;
f_->ui_vertex_bytes = vertex_bytes;
}
VulkanWindow::RenderTarget* VulkanWindow::get_render_target(const std::string& name) {
if (auto it = render_targets_.find(name); it != render_targets_.end()) return &it->second;
unsigned id = 0, w = 0, h = 0;
if (std::sscanf(name.c_str(), "rt:%u:%ux%u", &id, &w, &h) != 3 || !w || !h || w > 4096 || h > 4096)
return nullptr;
RenderTarget& target = render_targets_[name];
target.width = w;
target.height = h;
auto make_image = [&](VkFormat format, VkImageUsageFlags usage, VkImageAspectFlags aspect,
VkImage& image, VkDeviceMemory& memory, VkImageView& view) {
VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
info.imageType = VK_IMAGE_TYPE_2D;
info.format = format;
info.extent = {w, h, 1};
info.mipLevels = 1; info.arrayLayers = 1;
info.samples = VK_SAMPLE_COUNT_1_BIT;
info.tiling = VK_IMAGE_TILING_OPTIMAL;
info.usage = usage;
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
check(vkCreateImage(device_, &info, nullptr, &image), "vkCreateImage render target");
VkMemoryRequirements reqs{};
vkGetImageMemoryRequirements(device_, image, &reqs);
VkMemoryAllocateInfo alloc{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
alloc.allocationSize = reqs.size;
alloc.memoryTypeIndex = find_memory_type(reqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
check(vkAllocateMemory(device_, &alloc, nullptr, &memory), "vkAllocateMemory render target");
check(vkBindImageMemory(device_, image, memory, 0), "vkBindImageMemory render target");
VkImageViewCreateInfo view_info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
view_info.image = image; view_info.viewType = VK_IMAGE_VIEW_TYPE_2D; view_info.format = format;
view_info.subresourceRange = {aspect, 0, 1, 0, 1};
check(vkCreateImageView(device_, &view_info, nullptr, &view), "vkCreateImageView render target");
};
make_image(offscreen_format_,
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT |
VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
VK_IMAGE_ASPECT_COLOR_BIT, target.texture.image, target.texture.memory, target.texture.view);
make_image(VK_FORMAT_D32_SFLOAT,
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
VK_IMAGE_ASPECT_DEPTH_BIT, target.depth_image, target.depth_memory, target.depth_view);
const VkImageView views[2] = {target.texture.view, target.depth_view};
VkFramebufferCreateInfo fb{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
fb.renderPass = offscreen_pass_;
fb.attachmentCount = 2; fb.pAttachments = views;
fb.width = w; fb.height = h; fb.layers = 1;
check(vkCreateFramebuffer(device_, &fb, nullptr, &target.framebuffer), "vkCreateFramebuffer render target");
target.texture.descriptor = allocate_texture_descriptor_set(target.texture.view, fallback_texture_.view);
target.texture.ui_descriptor = allocate_ui_texture_descriptor_set(target.texture.view, fallback_texture_.view);
return &target;
}
void VulkanWindow::release_render_target(RenderTarget& target) {
if (target.framebuffer) vkDestroyFramebuffer(device_, target.framebuffer, nullptr);
if (target.depth_view) vkDestroyImageView(device_, target.depth_view, nullptr);
if (target.depth_image) vkDestroyImage(device_, target.depth_image, nullptr);
if (target.depth_memory) vkFreeMemory(device_, target.depth_memory, nullptr);
release_texture(target.texture);
target = {};
}
void VulkanWindow::initialize_render_target(RenderTarget& target) {
VkImageMemoryBarrier barriers[2]{};
for (auto& b : barriers) {
b.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
b.srcQueueFamilyIndex = b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
b.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
b.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
b.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
}
barriers[0].image = target.texture.image;
barriers[0].subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
barriers[1].image = target.depth_image;
barriers[1].subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1};
vkCmdPipelineBarrier(f_->command, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
0, 0, nullptr, 0, nullptr, 2, barriers);
const VkClearColorValue white{{1.0f, 1.0f, 1.0f, 1.0f}};
const VkClearDepthStencilValue far{1.0f, 0};
vkCmdClearColorImage(f_->command, target.texture.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &white, 1,
&barriers[0].subresourceRange);
vkCmdClearDepthStencilImage(f_->command, target.depth_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &far, 1,
&barriers[1].subresourceRange);
for (auto& b : barriers) {
b.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
b.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
}
barriers[0].newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
barriers[0].dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT;
barriers[1].newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
barriers[1].dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT;
vkCmdPipelineBarrier(f_->command, VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT |
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
0, 0, nullptr, 0, nullptr, 2, barriers);
target.initialized = true;
}
void VulkanWindow::release_geometry(Geometry& geometry) {
if (geometry.buffer) vkDestroyBuffer(device_, geometry.buffer, nullptr);
if (geometry.memory) vkFreeMemory(device_, geometry.memory, nullptr);
geometry = {};
}
void VulkanWindow::upload_geometry(const Render3DDraw& draw, Geometry& geometry) {
const ScopedMs timer(timings_.geometry_upload_ms);
if (draw.positions.size() % 3 || draw.indices.size() % (draw.lines ? 2 : 3))
throw std::runtime_error("invalid native geometry array size");
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");
// 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);
for (std::size_t i = 0; i < count; ++i) {
vertices[i].position[0] = draw.positions[3*i];
vertices[i].position[1] = draw.positions[3*i+1];
vertices[i].position[2] = draw.positions[3*i+2];
vertices[i].rhw = i < draw.rhw.size() ? draw.rhw[i] : 1.0f;
vertices[i].vertex_fog = i < draw.vertex_fog.size() ? draw.vertex_fog[i] : 1.0f;
if (3*i + 2 < draw.normals.size()) {
vertices[i].normal[0] = draw.normals[3*i];
vertices[i].normal[1] = draw.normals[3*i+1];
vertices[i].normal[2] = draw.normals[3*i+2];
} else {
vertices[i].normal[0] = 0.0f;
vertices[i].normal[1] = 0.0f;
vertices[i].normal[2] = 1.0f;
}
if (2*i + 1 < draw.uv0.size()) {
vertices[i].uv[0] = draw.uv0[2*i];
vertices[i].uv[1] = draw.uv0[2*i+1];
} else {
vertices[i].uv[0] = 0.0f;
vertices[i].uv[1] = 0.0f;
}
const auto argb = i < draw.diffuse.size() ? draw.diffuse[i] : default_argb;
vertices[i].color[0] = float((argb >> 16) & 255) / 255.0f;
vertices[i].color[1] = float((argb >> 8) & 255) / 255.0f;
vertices[i].color[2] = float(argb & 255) / 255.0f;
vertices[i].color[3] = float((argb >> 24) & 255) / 255.0f;
if (has_skin) {
for (int k = 0; k < 4; ++k) {
vertices[i].joints[k] = draw.bone_indices[4*i + k];
vertices[i].weights[k] = draw.bone_weights[4*i + k];
}
} else {
vertices[i].joints[0] = vertices[i].joints[1] = vertices[i].joints[2] = vertices[i].joints[3] = 0;
vertices[i].weights[0] = 1.0f;
vertices[i].weights[1] = vertices[i].weights[2] = vertices[i].weights[3] = 0.0f;
}
if (2*i + 1 < draw.uv1.size()) {
vertices[i].mask_uv[0] = draw.uv1[2*i];
vertices[i].mask_uv[1] = draw.uv1[2*i+1];
} else {
vertices[i].mask_uv[0] = 0.0f;
vertices[i].mask_uv[1] = 0.0f;
}
}
for (auto index : draw.indices) if (index >= count) throw std::runtime_error("draw index exceeds vertex count");
geometry.index_offset = vertices.size() * sizeof(Vertex);
const auto bytes = geometry.index_offset + draw.indices.size() * sizeof(std::uint32_t);
if (bytes > 128 * 1024 * 1024) throw std::runtime_error("native geometry buffer exceeds 128 MiB");
VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
info.size = bytes; info.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT;
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
check(vkCreateBuffer(device_, &info, nullptr, &geometry.buffer), "vkCreateBuffer");
VkMemoryRequirements requirements{};
vkGetBufferMemoryRequirements(device_, geometry.buffer, &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, &geometry.memory), "vkAllocateMemory");
check(vkBindBufferMemory(device_, geometry.buffer, geometry.memory, 0), "vkBindBufferMemory");
void* mapped = nullptr;
check(vkMapMemory(device_, geometry.memory, 0, bytes, 0, &mapped), "vkMapMemory");
std::memcpy(mapped, vertices.data(), geometry.index_offset);
std::memcpy(static_cast<std::uint8_t*>(mapped) + geometry.index_offset,
draw.indices.data(), draw.indices.size() * sizeof(std::uint32_t));
vkUnmapMemory(device_, geometry.memory);
geometry.vertex_count = static_cast<std::uint32_t>(count);
geometry.index_count = static_cast<std::uint32_t>(draw.indices.size());
++upload_count_;
uploaded_bytes_ += bytes;
}
} // namespace mt_host
+422
View File
@@ -0,0 +1,422 @@
// VulkanWindow: texture decode/upload, samplers per D3D8 sampler state, texture descriptor sets and eviction.
#include "vulkan_window.h"
#include "draw_capture.h"
#include "host_util.h"
#include "texture_decode.h"
#include <algorithm>
#include <cmath>
#include <cstring>
namespace mt_host {
VkDescriptorSet VulkanWindow::allocate_texture_descriptor_set(VkImageView view0, VkImageView view1,
VkSampler sampler0,
VkSampler sampler1) {
VkDescriptorSet set = VK_NULL_HANDLE;
VkDescriptorSetAllocateInfo desc_alloc{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
desc_alloc.descriptorPool = descriptor_pool_;
desc_alloc.descriptorSetCount = 1;
desc_alloc.pSetLayouts = &descriptor_layout_;
check(vkAllocateDescriptorSets(device_, &desc_alloc, &set), "vkAllocateDescriptorSets");
VkDescriptorImageInfo image_infos[2]{};
image_infos[0].sampler = sampler0 ? sampler0 : sampler_;
image_infos[0].imageView = view0;
image_infos[0].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
image_infos[1].sampler = sampler1 ? sampler1 : (clamp_sampler_ ? clamp_sampler_ : sampler_);
image_infos[1].imageView = view1;
image_infos[1].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkWriteDescriptorSet writes[2]{};
for (int b = 0; b < 2; ++b) {
writes[b].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writes[b].dstSet = set;
writes[b].dstBinding = static_cast<std::uint32_t>(b);
writes[b].descriptorCount = 1;
writes[b].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
writes[b].pImageInfo = &image_infos[b];
}
vkUpdateDescriptorSets(device_, 2, writes, 0, nullptr);
return set;
}
VkDescriptorSet VulkanWindow::allocate_ui_texture_descriptor_set(VkImageView view0, VkImageView view1) {
VkDescriptorSet set = VK_NULL_HANDLE;
VkDescriptorSetAllocateInfo desc_alloc{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
desc_alloc.descriptorPool = descriptor_pool_;
desc_alloc.descriptorSetCount = 1;
desc_alloc.pSetLayouts = &descriptor_layout_;
check(vkAllocateDescriptorSets(device_, &desc_alloc, &set), "vkAllocateDescriptorSets ui");
VkDescriptorImageInfo image_infos[2]{};
image_infos[0].sampler = ui_sampler_ ? ui_sampler_ : sampler_;
image_infos[0].imageView = view0;
image_infos[0].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
image_infos[1].sampler = ui_sampler_ ? ui_sampler_ : (clamp_sampler_ ? clamp_sampler_ : sampler_);
image_infos[1].imageView = view1;
image_infos[1].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkWriteDescriptorSet writes[2]{};
for (int b = 0; b < 2; ++b) {
writes[b].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writes[b].dstSet = set;
writes[b].dstBinding = static_cast<std::uint32_t>(b);
writes[b].descriptorCount = 1;
writes[b].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
writes[b].pImageInfo = &image_infos[b];
}
vkUpdateDescriptorSets(device_, 2, writes, 0, nullptr);
return set;
}
VulkanWindow::GpuTexture& VulkanWindow::get_gpu_texture(
const std::string& texture_name,
const std::unordered_map<std::string, std::vector<std::uint8_t>>& capture_textures) {
if (texture_name.empty()) return fallback_texture_;
if (texture_name.rfind("rt:", 0) == 0) {
RenderTarget* target = get_render_target(texture_name);
return target ? target->texture : fallback_texture_;
}
auto [it, inserted] = textures_.try_emplace(texture_name);
it->second.last_used_frame = frame_number_;
if (!inserted && (it->second.view || frame_number_ - it->second.last_decode_attempt_frame < 60))
return it->second;
if (inserted) {
it->second.descriptor = fallback_texture_.descriptor;
it->second.ui_descriptor = fallback_texture_.ui_descriptor;
}
it->second.last_decode_attempt_frame = frame_number_;
mtimage::Image decoded;
if (auto raw = capture_textures.find(texture_name); raw != capture_textures.end() && !raw->second.empty()) {
decoded = decode_texture_bytes(raw->second.data(), raw->second.size());
}
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
if (!decoded.ok()) {
if (texture_name.rfind("mem:", 0) == 0) {
UIMemoryTexture mem_tex;
if (UIRenderMemoryTexture(texture_name, &mem_tex) && mem_tex.width > 0 && mem_tex.height > 0) {
const auto mtra = native_draw_capture::encode_raw_argb_as_mtra(
static_cast<std::uint32_t>(mem_tex.width),
static_cast<std::uint32_t>(mem_tex.height),
mem_tex.argb.data());
decoded = decode_texture_bytes(mtra.data(), mtra.size());
}
} else {
std::vector<std::uint8_t> pack_bytes;
if (read_live_pack_texture(texture_name, pack_bytes))
decoded = decode_texture_bytes(pack_bytes.data(), pack_bytes.size());
}
}
#endif
if (!decoded.ok()) return it->second;
// 40250 CGraphicImageTexture: DXT DDS -> CreateDDSTexture with the file's levels;
// other files -> D3DXCreateTextureFromFileInMemoryEx(D3DX_DEFAULT) = full chain;
// memory images (mem:, CreateTexture(w, h, 1, ...)) -> one level.
const bool is_memory_tex = texture_name.rfind("mem:", 0) == 0;
last_texture_upload_name_ = texture_name;
upload_texture(
decoded.w, decoded.h, decoded.rgba.data(), it->second, true,
!is_memory_tex && decoded.file_mip_levels == 0, &decoded.mips);
it->second.descriptor = allocate_texture_descriptor_set(it->second.view, fallback_texture_.view);
it->second.ui_descriptor = allocate_ui_texture_descriptor_set(it->second.view, fallback_texture_.view);
return it->second;
}
std::uint64_t VulkanWindow::sampler_state_key(const Render3DDraw& draw, int stage) const {
return (std::uint64_t(draw.address_u[stage] & 15u)) |
(std::uint64_t(draw.address_v[stage] & 15u) << 4) |
(std::uint64_t(draw.min_filter[stage] & 15u) << 8) |
(std::uint64_t(draw.mag_filter[stage] & 15u) << 12) |
(std::uint64_t(draw.mip_filter[stage] & 15u) << 16);
}
VkSampler VulkanWindow::sampler_for_draw(const Render3DDraw& draw, int stage) {
const auto key = sampler_state_key(draw, stage);
if (key == 0) return stage == 0 ? sampler_ : clamp_sampler_;
if (auto it = state_samplers_.find(key); it != state_samplers_.end()) return it->second;
auto address_mode = [](std::uint32_t mode) {
switch (mode) {
case 2: return VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT;
case 3: return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
case 4: return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER;
default: return VK_SAMPLER_ADDRESS_MODE_REPEAT;
}
};
VkSamplerCreateInfo info{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
info.addressModeU = address_mode(draw.address_u[stage]);
info.addressModeV = address_mode(draw.address_v[stage]);
info.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
info.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
info.minFilter = draw.min_filter[stage] == 1 ? VK_FILTER_NEAREST : VK_FILTER_LINEAR;
info.magFilter = draw.mag_filter[stage] == 1 ? VK_FILTER_NEAREST : VK_FILTER_LINEAR;
info.mipmapMode = draw.mip_filter[stage] == 1 ? VK_SAMPLER_MIPMAP_MODE_NEAREST : VK_SAMPLER_MIPMAP_MODE_LINEAR;
info.minLod = 0.0f;
info.maxLod = draw.mip_filter[stage] == 0 ? 0.0f : VK_LOD_CLAMP_NONE;
if ((draw.min_filter[stage] == 3 || draw.mag_filter[stage] == 3) && max_anisotropy_ > 1.0f) {
info.anisotropyEnable = VK_TRUE;
info.maxAnisotropy = max_anisotropy_;
}
VkSampler sampler = VK_NULL_HANDLE;
check(vkCreateSampler(device_, &info, nullptr, &sampler), "vkCreateSampler D3D state");
state_samplers_.emplace(key, sampler);
return sampler;
}
VkDescriptorSet VulkanWindow::get_texture_descriptor(
const std::string& texture0_name,
const std::string& mask_name,
const std::unordered_map<std::string, std::vector<std::uint8_t>>& capture_textures,
const Render3DDraw& draw) {
const auto& tex0 = get_gpu_texture(texture0_name, capture_textures);
const auto& tex1 = mask_name.empty() ? fallback_texture_ : get_gpu_texture(mask_name, capture_textures);
const std::string pair_key = "3d\n" + texture0_name + "\n" + mask_name + "\n" +
std::to_string(sampler_state_key(draw, 0)) + "\n" + std::to_string(sampler_state_key(draw, 1));
if (auto it = paired_descriptors_.find(pair_key); it != paired_descriptors_.end()) {
it->second.last_used_frame = frame_number_;
return it->second.set;
}
const VkDescriptorSet set = allocate_texture_descriptor_set(
tex0.view ? tex0.view : fallback_texture_.view,
tex1.view ? tex1.view : fallback_texture_.view,
sampler_for_draw(draw, 0), sampler_for_draw(draw, 1));
paired_descriptors_.emplace(pair_key, PairedDescriptor{set, frame_number_});
return set;
}
VkDescriptorSet VulkanWindow::get_ui_texture_descriptor(
const std::string& texture0_name,
const std::string& mask_name,
const std::unordered_map<std::string, std::vector<std::uint8_t>>& capture_textures) {
const auto& tex0 = get_gpu_texture(texture0_name, capture_textures);
if (mask_name.empty()) return tex0.ui_descriptor ? tex0.ui_descriptor : tex0.descriptor;
const auto& tex1 = get_gpu_texture(mask_name, capture_textures);
if (!tex0.view || !tex1.view) return tex0.ui_descriptor ? tex0.ui_descriptor : tex0.descriptor;
const std::string pair_key = "ui\n" + texture0_name + "\n" + mask_name;
if (auto it = paired_descriptors_.find(pair_key); it != paired_descriptors_.end()) {
it->second.last_used_frame = frame_number_;
return it->second.set;
}
const VkDescriptorSet set = allocate_ui_texture_descriptor_set(
tex0.view ? tex0.view : fallback_texture_.view,
tex1.view ? tex1.view : fallback_texture_.view);
paired_descriptors_.emplace(pair_key, PairedDescriptor{set, frame_number_});
return set;
}
void VulkanWindow::upload_texture(
std::uint32_t width,
std::uint32_t height,
const std::uint8_t* rgba,
GpuTexture& texture,
bool count_upload,
bool generate_mips,
const std::vector<std::vector<std::uint8_t>>* file_mips) {
const ScopedMs timer(timings_.texture_upload_ms);
const bool has_file_mips = !generate_mips && file_mips && !file_mips->empty();
VkDeviceSize byte_size = VkDeviceSize(width) * height * 4;
if (has_file_mips)
for (const auto& level : *file_mips) byte_size += level.size();
const std::uint32_t mip_levels = generate_mips
? (static_cast<std::uint32_t>(std::floor(std::log2(std::max(width, height)))) + 1u)
: has_file_mips ? 1u + static_cast<std::uint32_t>(file_mips->size()) : 1u;
// Inside a frame the copy is recorded ahead of that frame's render pass and reads the slot's
// staging ring; the GPU runs it with the frame, no queue wait. Outside a frame (the fallback
// texture at startup) it is a one-off submit that waits.
const bool in_frame = f_->recording;
VkBuffer staging_buffer = VK_NULL_HANDLE;
VkDeviceMemory staging_memory = VK_NULL_HANDLE;
VkDeviceSize staging_offset = 0;
std::uint8_t* mapped = nullptr;
const VkDeviceSize aligned_offset =
(f_->staging_used + staging_alignment_ - 1) / staging_alignment_ * staging_alignment_;
if (in_frame && f_->staging_mapped && aligned_offset + byte_size <= f_->staging_capacity) {
staging_buffer = f_->staging_buffer;
staging_offset = aligned_offset;
mapped = f_->staging_mapped + staging_offset;
f_->staging_used = aligned_offset + byte_size;
} else {
if (in_frame) f_->staging_wanted = std::max(f_->staging_wanted, aligned_offset + byte_size);
void* raw = nullptr;
create_host_buffer(byte_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, staging_buffer, staging_memory, &raw);
mapped = static_cast<std::uint8_t*>(raw);
}
std::memcpy(mapped, rgba, VkDeviceSize(width) * height * 4);
if (has_file_mips) {
auto* dst = mapped + VkDeviceSize(width) * height * 4;
for (const auto& level : *file_mips) { std::memcpy(dst, level.data(), level.size()); dst += level.size(); }
}
if (staging_memory) vkUnmapMemory(device_, staging_memory);
VkImageCreateInfo image_info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
image_info.imageType = VK_IMAGE_TYPE_2D;
image_info.format = VK_FORMAT_R8G8B8A8_UNORM;
image_info.extent = {width, height, 1};
image_info.mipLevels = mip_levels;
image_info.arrayLayers = 1;
image_info.samples = VK_SAMPLE_COUNT_1_BIT;
image_info.tiling = VK_IMAGE_TILING_OPTIMAL;
image_info.usage =
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
check(vkCreateImage(device_, &image_info, nullptr, &texture.image), "vkCreateImage texture");
VkMemoryRequirements image_reqs{};
vkGetImageMemoryRequirements(device_, texture.image, &image_reqs);
VkMemoryAllocateInfo image_alloc{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
image_alloc.allocationSize = image_reqs.size;
image_alloc.memoryTypeIndex = find_memory_type(image_reqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
check(vkAllocateMemory(device_, &image_alloc, nullptr, &texture.memory), "vkAllocateMemory texture");
check(vkBindImageMemory(device_, texture.image, texture.memory, 0), "vkBindImageMemory texture");
VkCommandBuffer upload_cmd = f_->command;
if (!in_frame) {
VkCommandBufferAllocateInfo cmd_alloc{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
cmd_alloc.commandPool = pool_; cmd_alloc.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cmd_alloc.commandBufferCount = 1;
check(vkAllocateCommandBuffers(device_, &cmd_alloc, &upload_cmd), "vkAllocateCommandBuffers texture");
VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
check(vkBeginCommandBuffer(upload_cmd, &begin), "vkBeginCommandBuffer texture");
}
VkImageMemoryBarrier to_transfer{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
to_transfer.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
to_transfer.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
to_transfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
to_transfer.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
to_transfer.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
to_transfer.image = texture.image;
to_transfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, mip_levels, 0, 1};
vkCmdPipelineBarrier(
upload_cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
0, 0, nullptr, 0, nullptr, 1, &to_transfer);
std::vector<VkBufferImageCopy> regions(has_file_mips ? mip_levels : 1u);
VkDeviceSize region_offset = 0;
for (std::uint32_t i = 0; i < regions.size(); ++i) {
const std::uint32_t lw = std::max(1u, width >> i), lh = std::max(1u, height >> i);
regions[i].bufferOffset = staging_offset + region_offset;
regions[i].imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, i, 0, 1};
regions[i].imageExtent = {lw, lh, 1};
region_offset += VkDeviceSize(lw) * lh * 4;
}
vkCmdCopyBufferToImage(
upload_cmd, staging_buffer, texture.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
static_cast<std::uint32_t>(regions.size()), regions.data());
std::int32_t mip_w = static_cast<std::int32_t>(width);
std::int32_t mip_h = static_cast<std::int32_t>(height);
for (std::uint32_t i = 1; generate_mips && i < mip_levels; ++i) {
VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
barrier.image = texture.image;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, i - 1, 1, 0, 1};
barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
vkCmdPipelineBarrier(
upload_cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
0, 0, nullptr, 0, nullptr, 1, &barrier);
const std::int32_t next_w = std::max(1, mip_w / 2);
const std::int32_t next_h = std::max(1, mip_h / 2);
VkImageBlit blit{};
blit.srcOffsets[0] = {0, 0, 0};
blit.srcOffsets[1] = {mip_w, mip_h, 1};
blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, i - 1, 0, 1};
blit.dstOffsets[0] = {0, 0, 0};
blit.dstOffsets[1] = {next_w, next_h, 1};
blit.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, i, 0, 1};
vkCmdBlitImage(
upload_cmd,
texture.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
texture.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1, &blit, VK_FILTER_LINEAR);
barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
vkCmdPipelineBarrier(
upload_cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
0, 0, nullptr, 0, nullptr, 1, &barrier);
mip_w = next_w;
mip_h = next_h;
}
VkImageMemoryBarrier to_shader{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
to_shader.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
to_shader.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
to_shader.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
to_shader.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
to_shader.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
to_shader.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
to_shader.image = texture.image;
to_shader.subresourceRange = generate_mips
? VkImageSubresourceRange{VK_IMAGE_ASPECT_COLOR_BIT, mip_levels - 1, 1, 0, 1}
: VkImageSubresourceRange{VK_IMAGE_ASPECT_COLOR_BIT, 0, mip_levels, 0, 1};
vkCmdPipelineBarrier(
upload_cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
0, 0, nullptr, 0, nullptr, 1, &to_shader);
if (in_frame) {
if (staging_memory) f_->retired_buffers.emplace_back(staging_buffer, staging_memory);
} else {
check(vkEndCommandBuffer(upload_cmd), "vkEndCommandBuffer texture");
VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO};
submit.commandBufferCount = 1; submit.pCommandBuffers = &upload_cmd;
check(vkQueueSubmit(queue_, 1, &submit, VK_NULL_HANDLE), "vkQueueSubmit texture");
check(vkQueueWaitIdle(queue_), "vkQueueWaitIdle texture");
vkFreeCommandBuffers(device_, pool_, 1, &upload_cmd);
vkDestroyBuffer(device_, staging_buffer, nullptr);
vkFreeMemory(device_, staging_memory, nullptr);
}
VkImageViewCreateInfo view_info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
view_info.image = texture.image; view_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
view_info.format = VK_FORMAT_R8G8B8A8_UNORM;
view_info.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, mip_levels, 0, 1};
check(vkCreateImageView(device_, &view_info, nullptr, &texture.view), "vkCreateImageView texture");
if (count_upload) {
++texture_upload_count_;
texture_uploaded_bytes_ += byte_size;
}
}
void VulkanWindow::release_texture(GpuTexture& texture) {
for (VkDescriptorSet set : {texture.descriptor, texture.ui_descriptor})
if (set && set != fallback_texture_.descriptor && set != fallback_texture_.ui_descriptor)
vkFreeDescriptorSets(device_, descriptor_pool_, 1, &set);
if (texture.view) vkDestroyImageView(device_, texture.view, nullptr);
if (texture.image) vkDestroyImage(device_, texture.image, nullptr);
if (texture.memory) vkFreeMemory(device_, texture.memory, nullptr);
texture = {};
}
void VulkanWindow::prune_textures() {
constexpr std::uint64_t static_retention_frames = 120;
constexpr std::uint64_t memory_retention_frames = 2;
for (auto it = paired_descriptors_.begin(); it != paired_descriptors_.end();) {
const auto retention = it->first.find("mem:") != std::string::npos
? memory_retention_frames : static_retention_frames;
if (frame_number_ - it->second.last_used_frame > retention) {
vkFreeDescriptorSets(device_, descriptor_pool_, 1, &it->second.set);
it = paired_descriptors_.erase(it);
} else ++it;
}
for (auto it = textures_.begin(); it != textures_.end();) {
const auto retention = it->first.rfind("mem:", 0) == 0
? memory_retention_frames : static_retention_frames;
if (frame_number_ - it->second.last_used_frame > retention) {
release_texture(it->second);
it = textures_.erase(it);
} else ++it;
}
}
} // namespace mt_host
File diff suppressed because it is too large Load Diff
+38
View File
@@ -196,6 +196,44 @@ public:
native_perf::RendererTotals perf_totals() const; native_perf::RendererTotals perf_totals() const;
private: private:
using TextureBytes = std::unordered_map<std::string, std::vector<std::uint8_t>>;
// render()'s phases, in call order (vk_frame.cpp).
struct FrameDraws {
std::vector<PreparedDraw> prepared_draws; // the back-buffer pass
// Draws into offscreen render targets (the shadow map), in recorded order, drawn before the
// back-buffer pass that samples them.
std::vector<PreparedDraw> offscreen_draws;
std::size_t vertex_count = 0, index_count = 0, skinned_draw_count = 0;
};
// Host copies of the swapchain image (--screenshot-out) and, with MT_DUMP_RT, of every render target.
struct Readback {
struct RtDump { VkBuffer buffer; VkDeviceMemory memory; std::uint32_t w, h; };
bool screenshot = false;
VkBuffer buffer = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
const char* dump_rt = nullptr;
std::vector<RtDump> rt_dumps;
};
// Geometry, pipeline and descriptors of each recorded draw; retires unused geometry.
void prepare_draws(const std::vector<Render3DDraw>& draws, const TextureBytes& capture_textures, FrameDraws& frame);
// The UI batches, with the touch controls and FPS counter appended to the 40250 UI.
void prepare_ui(std::uint32_t ui_width, std::uint32_t ui_height, const std::vector<UIRenderCommand>& ui_commands,
const TextureBytes& capture_textures, std::vector<UiBatch>& ui_batches, std::size_t& ui_quad_count);
// Each draw's FixedFunctionState into the frame's dynamic-offset state buffer.
void write_draw_states(FrameDraws& frame, std::vector<UiBatch>& ui_batches);
// Offscreen passes, then the back-buffer pass: UI behind 3D, 3D, UI.
void record_passes(std::uint32_t image_index, const FrameDraws& frame, const std::vector<UiBatch>& ui_batches);
// Recorded after the passes; finish_readbacks() waits for the submit and writes the files.
Readback record_readbacks(std::uint32_t image_index);
void finish_readbacks(Readback& readback);
void recreate_swapchain(); void recreate_swapchain();
void collect_pending_gpu_timestamp(FrameSlot& slot); void collect_pending_gpu_timestamp(FrameSlot& slot);