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>
198 lines
11 KiB
C++
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
|