Files
mtgodot-poc/src/host/vk_scene.cpp
T
shenleiandClaude Opus 5.5 44a020f01e native renderer: game world at MT_RENDER_SCALE (0.75 on Android), MSAA default 2
Draws recorded inside CPythonApplication::RenderGame are marked Render3DDraw::scene and
drawn by a scene pass compatible with pass_ into a reduced-resolution image, composited
where RenderGame drew; the UI stays full resolution. --render-scale / MT_RENDER_SCALE,
desktop default 1.0 (unchanged path).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:28:05 +09:00

198 lines
11 KiB
C++

// VulkanWindow: the reduced-resolution scene pass (MT_RENDER_SCALE). The game world's draws
// (Render3DDraw::scene, recorded inside CPythonApplication::RenderGame) go into scene_texture_ at
// render_scale_ of the swapchain extent; a quad composites it where RenderGame drew, under the UI.
#include "vulkan_window.h"
#include "host_util.h"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <stdexcept>
namespace mt_host {
void VulkanWindow::create_scene_pass() {
#ifdef __ANDROID__
render_scale_ = 0.75f;
#else
render_scale_ = 1.0f;
#endif
if (const char* env = std::getenv("MT_RENDER_SCALE")) render_scale_ = float(std::atof(env));
render_scale_ = std::clamp(render_scale_, 0.25f, 1.0f);
VkFormatProperties properties{};
vkGetPhysicalDeviceFormatProperties(physical_, swapchain_format_, &properties);
const VkFormatFeatureFlags wanted = VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT |
VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT;
scene_supported_ = (properties.optimalTilingFeatures & wanted) == wanted;
if (!scene_supported_ || render_scale_ >= 1.0f) return;
// Same formats, sample counts and resolve layout as pass_, so its pipelines are compatible.
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_SHADER_READ_ONLY_OPTIMAL;
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;
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_SHADER_READ_ONLY_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 dependencies[2]{};
// The previous frame's composite samples the image this pass overwrites.
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;
dependencies[0].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_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 = msaa ? 3 : 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, &scene_pass_), "vkCreateRenderPass scene");
// Two triangles over the unit square; scene_composite_draw maps it to the whole back buffer.
Render3DDraw quad{};
quad.pretransformed = true;
quad.positions = {0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0};
quad.rhw = {1, 1, 1, 1};
quad.uv0 = {0, 0, 1, 0, 0, 1, 1, 1};
quad.diffuse = {0xffffffffu, 0xffffffffu, 0xffffffffu, 0xffffffffu};
quad.indices = {0, 1, 2, 2, 1, 3};
upload_geometry(quad, scene_quad_);
}
void VulkanWindow::destroy_scene_target() {
if (scene_framebuffer_) vkDestroyFramebuffer(device_, scene_framebuffer_, nullptr);
if (scene_msaa_view_) vkDestroyImageView(device_, scene_msaa_view_, nullptr);
if (scene_msaa_image_) vkDestroyImage(device_, scene_msaa_image_, nullptr);
if (scene_msaa_memory_) vkFreeMemory(device_, scene_msaa_memory_, nullptr);
if (scene_depth_view_) vkDestroyImageView(device_, scene_depth_view_, nullptr);
if (scene_depth_image_) vkDestroyImage(device_, scene_depth_image_, nullptr);
if (scene_depth_memory_) vkFreeMemory(device_, scene_depth_memory_, nullptr);
release_texture(scene_texture_);
scene_framebuffer_ = VK_NULL_HANDLE;
scene_msaa_view_ = VK_NULL_HANDLE; scene_msaa_image_ = VK_NULL_HANDLE; scene_msaa_memory_ = VK_NULL_HANDLE;
scene_depth_view_ = VK_NULL_HANDLE; scene_depth_image_ = VK_NULL_HANDLE; scene_depth_memory_ = VK_NULL_HANDLE;
scene_extent_ = {};
}
void VulkanWindow::ensure_scene_target() {
const VkExtent2D wanted{std::max(1u, std::uint32_t(std::lround(extent_.width * render_scale_))),
std::max(1u, std::uint32_t(std::lround(extent_.height * render_scale_)))};
if (scene_framebuffer_ && wanted.width == scene_extent_.width && wanted.height == scene_extent_.height) return;
if (scene_framebuffer_) {
vkDeviceWaitIdle(device_);
destroy_scene_target();
}
scene_extent_ = wanted;
auto make_image = [&](VkFormat format, VkSampleCountFlagBits samples, VkImageUsageFlags usage,
VkImageAspectFlags aspect, bool transient,
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 = {wanted.width, wanted.height, 1};
info.mipLevels = 1; info.arrayLayers = 1;
info.samples = samples;
info.tiling = VK_IMAGE_TILING_OPTIMAL;
info.usage = usage;
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
check(vkCreateImage(device_, &info, nullptr, &image), "vkCreateImage scene");
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);
if (transient) {
try {
alloc.memoryTypeIndex = find_memory_type(reqs.memoryTypeBits, VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT);
} catch (const std::runtime_error&) {}
}
check(vkAllocateMemory(device_, &alloc, nullptr, &memory), "vkAllocateMemory scene");
check(vkBindImageMemory(device_, image, memory, 0), "vkBindImageMemory scene");
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 scene");
};
const bool msaa = samples_ != VK_SAMPLE_COUNT_1_BIT;
make_image(swapchain_format_, VK_SAMPLE_COUNT_1_BIT,
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_ASPECT_COLOR_BIT, false,
scene_texture_.image, scene_texture_.memory, scene_texture_.view);
if (msaa)
make_image(swapchain_format_, samples_,
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT,
VK_IMAGE_ASPECT_COLOR_BIT, true, scene_msaa_image_, scene_msaa_memory_, scene_msaa_view_);
make_image(VK_FORMAT_D32_SFLOAT, samples_,
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT,
VK_IMAGE_ASPECT_DEPTH_BIT, true, scene_depth_image_, scene_depth_memory_, scene_depth_view_);
const VkImageView views[3] = {msaa ? scene_msaa_view_ : scene_texture_.view, scene_depth_view_, scene_texture_.view};
VkFramebufferCreateInfo fb{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
fb.renderPass = scene_pass_;
fb.attachmentCount = msaa ? 3 : 2; fb.pAttachments = views;
fb.width = wanted.width; fb.height = wanted.height; fb.layers = 1;
check(vkCreateFramebuffer(device_, &fb, nullptr, &scene_framebuffer_), "vkCreateFramebuffer scene");
// Bilinear, clamped: the upscale to the back buffer.
Render3DDraw sampling{};
sampling.min_filter[0] = sampling.mag_filter[0] = 2; // D3DTEXF_LINEAR
sampling.address_u[0] = sampling.address_v[0] = 3; // D3DTADDRESS_CLAMP
scene_texture_.descriptor = allocate_texture_descriptor_set(
scene_texture_.view, fallback_texture_.view, sampler_for_draw(sampling, 0), VK_NULL_HANDLE);
}
VulkanWindow::PreparedDraw VulkanWindow::scene_composite_draw() {
// Stage 0 SELECTARG1(TEXTURE) for colour and alpha, stage 1 disabled, no lighting or fog.
Render3DDraw quad{};
quad.pretransformed = true;
quad.texture0 = "scene";
quad.diffuse = {0xffffffffu};
quad.color_op[0] = quad.alpha_op[0] = 2;
quad.color_arg1[0] = quad.alpha_arg1[0] = 2;
quad.color_op[1] = quad.alpha_op[1] = 1;
for (float* m : {quad.world, quad.view, quad.proj})
for (int i = 0; i < 16; ++i) m[i] = i % 5 == 0 ? 1.0f : 0.0f;
PreparedDraw composite{};
composite.geometry = &scene_quad_;
composite.pipeline = get_pipeline(0, 0, 0);
composite.descriptor = scene_texture_.descriptor;
composite.constants = make_push_constants(quad, 0.0f);
// Unit square (y down) to D3D NDC (y up), as the pretransformed draws map screen pixels.
composite.constants.mvp = {2, 0, 0, 0,
0, -2, 0, 0,
0, 0, 1, 0,
-1, 1, 0, 1};
composite.fixed_state = make_fixed_function_state(quad);
composite.viewport = full_viewport();
return composite;
}
} // namespace mt_host