// 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 #include #include #include 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