From 44a020f01eea7868b7eebd46f03f022f4b90b062 Mon Sep 17 00:00:00 2001 From: shenlei Date: Wed, 30 Sep 2026 11:28:05 +0900 Subject: [PATCH] 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 --- src/host/CMakeLists.txt | 1 + src/host/live_client.cpp | 3 +- src/host/main.cpp | 3 +- src/host/vk_device.cpp | 3 +- src/host/vk_frame.cpp | 60 +++++- src/host/vk_pipelines.cpp | 1 + src/host/vk_scene.cpp | 197 ++++++++++++++++++ src/host/vulkan_window.cpp | 4 + src/host/vulkan_window.h | 32 +++ src/platform/EterLib/RecordingDevice.cpp | 5 + src/platform/EterLib/RenderCommands3D.h | 7 + .../UserInterface/PythonApplication.cpp | 2 + 12 files changed, 307 insertions(+), 11 deletions(-) create mode 100644 src/host/vk_scene.cpp diff --git a/src/host/CMakeLists.txt b/src/host/CMakeLists.txt index 9f285dbc..73428867 100644 --- a/src/host/CMakeLists.txt +++ b/src/host/CMakeLists.txt @@ -29,6 +29,7 @@ set(MT_NATIVE_RENDER_SOURCES vk_device.cpp vk_frame.cpp vk_pipelines.cpp + vk_scene.cpp vk_resources.cpp vk_textures.cpp stb_image_impl.cpp diff --git a/src/host/live_client.cpp b/src/host/live_client.cpp index 2d287430..c727edc4 100644 --- a/src/host/live_client.cpp +++ b/src/host/live_client.cpp @@ -373,7 +373,8 @@ int run_live_client( native_perf::PerfLog perf_log; if (g_perf_log) { perf_log.open("device=" + renderer.device_name() + " present_mode=" + renderer.present_mode_name() + - " msaa=" + std::to_string(renderer.msaa_samples()) + " size=" + std::to_string(renderer.width()) + + " msaa=" + std::to_string(renderer.msaa_samples()) + + " render_scale=" + std::to_string(renderer.render_scale()) + " size=" + std::to_string(renderer.width()) + "x" + std::to_string(renderer.height()) + " gpu_skinning=" + std::to_string(gpu_skinning) + " terrain=" + std::to_string(native_terrain)); } diff --git a/src/host/main.cpp b/src/host/main.cpp index 7a8f0b86..86aefd91 100644 --- a/src/host/main.cpp +++ b/src/host/main.cpp @@ -147,6 +147,7 @@ int main(int argc, char** argv) { else if (arg == "--fake-idle-ms" && i+1 < argc) setenv("MT_FAKE_IDLE_MS", argv[++i], 1); else if (arg == "--frames-in-flight" && i+1 < argc) frames_in_flight = std::stoi(argv[++i]); else if (arg == "--msaa" && i+1 < argc) setenv("MT_MSAA", argv[++i], 1); + else if (arg == "--render-scale" && i+1 < argc) setenv("MT_RENDER_SCALE", argv[++i], 1); else if (arg == "--cpu-shadow") setenv("MT_CPU_SHADOW", "1", 1); else throw std::runtime_error( "usage: mt_native_render [--frames N] [--interactive] [--login-screen|--selection-screen] " @@ -154,7 +155,7 @@ int main(int argc, char** argv) { "[--draws N] [--triangles-per-draw N] [--capture FILE] [--animate-first-draw] " "[--animate-bones] [--no-vsync] [--live-client DIR] [--fake-mobs N] " "[--gpu-skinning|--no-gpu-skinning] [--no-terrain] [--mobile] [--capture-out FILE] [--screenshot-out FILE.bmp] [--show-fps] [--perf-log] " - "[--fps-cap N] [--refresh-rate HZ] [--fps-mode 0|1|2] [--idle-fps N] [--idle-after S] [--frames-in-flight 1|2] [--msaa 1|2|4|8] [--fake-idle-ms MS] [--cpu-shadow]"); + "[--fps-cap N] [--refresh-rate HZ] [--fps-mode 0|1|2] [--idle-fps N] [--idle-after S] [--frames-in-flight 1|2] [--msaa 1|2|4|8] [--render-scale 0.25..1] [--fake-idle-ms MS] [--cpu-shadow]"); } #ifdef MT_NATIVE_HAS_LIVE_CLIENT if (native_perf::PerfLog::requested_by_env()) g_perf_log = true; diff --git a/src/host/vk_device.cpp b/src/host/vk_device.cpp index 26ab8c0b..d08f49ad 100644 --- a/src/host/vk_device.cpp +++ b/src/host/vk_device.cpp @@ -53,7 +53,7 @@ void VulkanWindow::select_device() { } // Multisampled 3D edges; MT_MSAA=1 keeps the single-sample path, 2/4/8 request a count. { - int wanted = 4; + int wanted = 2; if (const char* env = std::getenv("MT_MSAA")) wanted = std::max(1, std::atoi(env)); const VkSampleCountFlags supported = properties.limits.framebufferColorSampleCounts & properties.limits.framebufferDepthSampleCounts; @@ -186,6 +186,7 @@ void VulkanWindow::recreate_swapchain() { 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(); + destroy_scene_target(); for (auto view : image_views_) vkDestroyImageView(device_, view, nullptr); image_views_.clear(); if (swapchain_) { vkDestroySwapchainKHR(device_, swapchain_, nullptr); swapchain_ = VK_NULL_HANDLE; } diff --git a/src/host/vk_frame.cpp b/src/host/vk_frame.cpp index e3b837e2..3f747588 100644 --- a/src/host/vk_frame.cpp +++ b/src/host/vk_frame.cpp @@ -110,7 +110,7 @@ void VulkanWindow::render( if (timed_frames_ > 1) timings_.steady_prepare_ms += prepare_ms; timings_.submit_ms += std::chrono::duration(submitted - prepared).count(); timings_.present_ms += std::chrono::duration(presented_at - submitted).count(); - last_draw_count_ = frame.prepared_draws.size(); + last_draw_count_ = frame.prepared_draws.size() + frame.scene_draws.size(); last_skinned_draw_count_ = frame.skinned_draw_count; last_ui_batch_count_ = ui_batches.size(); last_ui_quad_count_ = ui_quad_count; @@ -134,7 +134,28 @@ void VulkanWindow::prepare_draws(const std::vector& draws, const T } ensure_bone_capacity(required_bones); + // MT_RENDER_SCALE: the game world into the reduced scene image (vk_scene.cpp). + bool scaled_scene = false; + if (scene_pass_ && render_scale_ < 1.0f) + scaled_scene = std::any_of(draws.begin(), draws.end(), + [](const Render3DDraw& d) { return d.scene && !d.render_target; }); + if (scaled_scene) ensure_scene_target(); + const float scene_sx = scaled_scene ? float(scene_extent_.width) / float(extent_.width) : 1.0f; + const float scene_sy = scaled_scene ? float(scene_extent_.height) / float(extent_.height) : 1.0f; + auto to_scene = [&](VkViewport viewport) { + viewport.x *= scene_sx; viewport.width *= scene_sx; + viewport.y *= scene_sy; viewport.height *= scene_sy; + return viewport; + }; + // The draw list a back-buffer draw goes to; the first scene draw leaves the composite in its place. + auto back_buffer_list = [&](bool in_scene) -> std::vector& { + if (!in_scene) return prepared_draws; + if (frame.scene_draws.empty()) prepared_draws.push_back(scene_composite_draw()); + return frame.scene_draws; + }; + for (const auto& draw : draws) { + const bool in_scene = scaled_scene && draw.scene && !draw.render_target; RenderTarget* target = nullptr; if (draw.render_target) { target = get_render_target( @@ -159,14 +180,15 @@ void VulkanWindow::prepare_draws(const std::vector& draws, const T 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)); + const auto viewport = in_scene ? to_scene(draw_viewport(draw)) : draw_viewport(draw); + const VkExtent2D bounds = in_scene ? scene_extent_ : extent_; + const float x0 = std::clamp(viewport.x, 0.0f, float(bounds.width)); + const float y0 = std::clamp(viewport.y, 0.0f, float(bounds.height)); + const float x1 = std::clamp(viewport.x + viewport.width, 0.0f, float(bounds.width)); + const float y1 = std::clamp(viewport.y + viewport.height, 0.0f, float(bounds.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); + back_buffer_list(in_scene).push_back(clear); continue; } if (draw.positions.empty() || draw.indices.empty()) continue; @@ -247,6 +269,9 @@ void VulkanWindow::prepare_draws(const std::vector& draws, const T 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 if (in_scene) { + prepared_draw.viewport = to_scene(prepared_draw.viewport); + back_buffer_list(true).push_back(prepared_draw); } else prepared_draws.push_back(prepared_draw); frame.vertex_count += geometry.vertex_count; frame.index_count += geometry.index_count; @@ -289,8 +314,15 @@ void VulkanWindow::prepare_ui(std::uint32_t ui_width, std::uint32_t ui_height, c } void VulkanWindow::write_draw_states(FrameDraws& frame, std::vector& ui_batches) { - ensure_state_capacity(frame.prepared_draws.size() + frame.offscreen_draws.size() + ui_batches.size()); + ensure_state_capacity(frame.prepared_draws.size() + frame.offscreen_draws.size() + frame.scene_draws.size() + + ui_batches.size()); std::size_t state_index = 0; + for (auto& draw : frame.scene_draws) { + if (!draw.geometry) continue; + draw.state_offset = static_cast(state_index * state_stride_); + std::memcpy(f_->state_mapped + draw.state_offset, &draw.fixed_state, sizeof(FixedFunctionState)); + ++state_index; + } for (auto& draw : frame.offscreen_draws) { if (!draw.geometry) continue; draw.state_offset = static_cast(state_index * state_stride_); @@ -391,6 +423,18 @@ void VulkanWindow::record_passes(std::uint32_t image_index, const FrameDraws& fr vkCmdEndRenderPass(f_->command); } + // The game world at render_scale(), sampled by the composite quad in the back-buffer pass. + if (!frame.scene_draws.empty()) { + VkRenderPassBeginInfo scene_begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + scene_begin.renderPass = scene_pass_; + scene_begin.framebuffer = scene_framebuffer_; + scene_begin.renderArea.extent = scene_extent_; + scene_begin.clearValueCount = 2; scene_begin.pClearValues = clears; + vkCmdBeginRenderPass(f_->command, &scene_begin, VK_SUBPASS_CONTENTS_INLINE); + for (const auto& prepared_draw : frame.scene_draws) record_prepared(prepared_draw); + 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; diff --git a/src/host/vk_pipelines.cpp b/src/host/vk_pipelines.cpp index 4cdc3bea..9a1fd010 100644 --- a/src/host/vk_pipelines.cpp +++ b/src/host/vk_pipelines.cpp @@ -92,6 +92,7 @@ void VulkanWindow::create_render_pass_and_layout() { get_pipeline(0, 2, 0); create_offscreen_pass(); + create_scene_pass(); } void VulkanWindow::create_offscreen_pass() { diff --git a/src/host/vk_scene.cpp b/src/host/vk_scene.cpp new file mode 100644 index 00000000..b61d213c --- /dev/null +++ b/src/host/vk_scene.cpp @@ -0,0 +1,197 @@ +// 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 diff --git a/src/host/vulkan_window.cpp b/src/host/vulkan_window.cpp index 1b93111a..a07a8069 100644 --- a/src/host/vulkan_window.cpp +++ b/src/host/vulkan_window.cpp @@ -120,6 +120,9 @@ VulkanWindow::~VulkanWindow() { for (auto& entry : textures_) release_texture(entry.second); for (auto& entry : render_targets_) release_render_target(entry.second); if (offscreen_pass_) vkDestroyRenderPass(device_, offscreen_pass_, nullptr); + destroy_scene_target(); + release_geometry(scene_quad_); + if (scene_pass_) vkDestroyRenderPass(device_, scene_pass_, nullptr); release_texture(fallback_texture_); for (auto& entry : pipelines_) vkDestroyPipeline(device_, entry.second, nullptr); if (vertex_module_) vkDestroyShaderModule(device_, vertex_module_, nullptr); @@ -229,6 +232,7 @@ void print_summary(const VulkanWindow& renderer, int completed, double wall_ms, }; std::cout << "device=" << renderer.device_name() << " present_mode=" << renderer.present_mode_name() << " msaa=" << renderer.msaa_samples() + << " render_scale=" << renderer.render_scale() << " frames=" << completed << " draws=" << renderer.draw_count() << " skinned_draws=" << renderer.skinned_draw_count() diff --git a/src/host/vulkan_window.h b/src/host/vulkan_window.h index 1fa0170c..6a7edca8 100644 --- a/src/host/vulkan_window.h +++ b/src/host/vulkan_window.h @@ -136,6 +136,9 @@ public: std::uint64_t frame_number() const { return frame_number_; } int msaa_samples() const { return int(samples_); } + // The game world's resolution relative to the swapchain (MT_RENDER_SCALE; 1 = drawn in the + // back-buffer pass as before). + float render_scale() const { return scene_pass_ ? render_scale_ : 1.0f; } std::uint32_t width() const { return extent_.width; } std::uint32_t height() const { return extent_.height; } std::uint32_t logical_width() const; @@ -201,6 +204,9 @@ private: // render()'s phases, in call order (vk_frame.cpp). struct FrameDraws { std::vector prepared_draws; // the back-buffer pass + // Render3DDraw::scene draws at render_scale(): drawn by the scene pass into scene_texture_, + // which a quad in prepared_draws (where the first of them was recorded) composites. + std::vector scene_draws; // Draws into offscreen render targets (the shadow map), in recorded order, drawn before the // back-buffer pass that samples them. std::vector offscreen_draws; @@ -282,6 +288,19 @@ private: // the device can render to it) + D32 depth, both loaded and stored, colour left shader-readable. void create_offscreen_pass(); + // The reduced-resolution scene pass: pass_'s attachments (so pass_ pipelines draw in it) at + // scene_extent_, resolving into the sampled scene_texture_. Unsupported when the swapchain + // format cannot be sampled; render_scale() is then 1. + void create_scene_pass(); + + // scene_texture_ and its attachments at render_scale_ of the current extent. + void ensure_scene_target(); + + void destroy_scene_target(); + + // The composite quad over the whole back buffer sampling scene_texture_. + PreparedDraw scene_composite_draw(); + // "rt::x" -> its render target, created on first use (by a draw into it or by a draw // sampling it); null for a malformed name. RenderTarget* get_render_target(const std::string& name); @@ -441,6 +460,19 @@ private: std::unordered_map paired_descriptors_; std::unordered_map render_targets_; VkRenderPass offscreen_pass_ = VK_NULL_HANDLE; + float render_scale_ = 1.0f; + bool scene_supported_ = false; + VkRenderPass scene_pass_ = VK_NULL_HANDLE; + VkExtent2D scene_extent_{}; + VkImage scene_msaa_image_ = VK_NULL_HANDLE; + VkDeviceMemory scene_msaa_memory_ = VK_NULL_HANDLE; + VkImageView scene_msaa_view_ = VK_NULL_HANDLE; + VkImage scene_depth_image_ = VK_NULL_HANDLE; + VkDeviceMemory scene_depth_memory_ = VK_NULL_HANDLE; + VkImageView scene_depth_view_ = VK_NULL_HANDLE; + VkFramebuffer scene_framebuffer_ = VK_NULL_HANDLE; + GpuTexture scene_texture_{}; + Geometry scene_quad_{}; VkFormat offscreen_format_ = VK_FORMAT_R5G6B5_UNORM_PACK16; std::size_t last_offscreen_draw_count_ = 0; VkShaderModule vertex_module_ = VK_NULL_HANDLE; diff --git a/src/platform/EterLib/RecordingDevice.cpp b/src/platform/EterLib/RecordingDevice.cpp index 2bcac33b..2a668e4f 100644 --- a/src/platform/EterLib/RecordingDevice.cpp +++ b/src/platform/EterLib/RecordingDevice.cpp @@ -15,6 +15,7 @@ namespace { std::mutex g_draws_mutex; std::vector g_draws; +bool g_in_scene = false; int g_native_terrain_override = -1; bool g_gpu_render_targets = false; @@ -1256,9 +1257,13 @@ void Render3DBeginFrame() void Render3DAdd(Render3DDraw draw) { std::lock_guard lock(g_draws_mutex); + draw.scene = g_in_scene; g_draws.push_back(std::move(draw)); } +void Render3DBeginScene() { g_in_scene = true; } +void Render3DEndScene() { g_in_scene = false; } + const std::vector& Render3DDraws() { return g_draws; } // D3D8 fixed-function texture coordinate processing for one stage (the native renderer's diff --git a/src/platform/EterLib/RenderCommands3D.h b/src/platform/EterLib/RenderCommands3D.h index f06e313c..0b575fd9 100644 --- a/src/platform/EterLib/RenderCommands3D.h +++ b/src/platform/EterLib/RenderCommands3D.h @@ -22,6 +22,9 @@ struct Render3DDraw { // PORT: UIRenderOffsetX() when recorded (a draw from a shifted window layer); the host moves the // viewport (and a pretransformed draw's screen positions) right by it. float ui_offset_x = 0; + // PORT: recorded inside CPythonApplication::RenderGame (Render3DBeginScene/EndScene). The native + // renderer may draw these into a reduced-resolution scene image composited under the UI. + bool scene = false; // IDirect3DDevice8::Clear on the back buffer, kept in draw order. A clear entry has no geometry; // clear_flags holds D3DCLEAR_TARGET / D3DCLEAR_ZBUFFER / D3DCLEAR_STENCIL. @@ -138,4 +141,8 @@ std::string Render3DRenderTargetName(std::uint32_t id, std::uint32_t width, std: void Render3DBeginFrame(); void Render3DAdd(Render3DDraw draw); +// PORT: brackets the game world's draws (CPythonApplication::RenderGame); Render3DAdd marks the draws +// in between with Render3DDraw::scene. +void Render3DBeginScene(); +void Render3DEndScene(); const std::vector& Render3DDraws(); diff --git a/src/platform/UserInterface/PythonApplication.cpp b/src/platform/UserInterface/PythonApplication.cpp index 254d352e..5ca9f711 100644 --- a/src/platform/UserInterface/PythonApplication.cpp +++ b/src/platform/UserInterface/PythonApplication.cpp @@ -202,6 +202,8 @@ void CPythonApplication::GetMousePosition(POINT* ppt) void CPythonApplication::RenderGame() { MtPerf::CScope kPerf(MtPerf::SECTION_RENDER_GAME); + // PORT: marks the world's draws so the native renderer can draw them at a reduced resolution. + struct SceneScope { SceneScope() { Render3DBeginScene(); } ~SceneScope() { Render3DEndScene(); } } kScene; float fAspect=UI::CWindowManager::Instance().GetAspect(); float fFarClip=CPythonBackground::Instance().GetFarClip();