From 52ccb80d0178a66bc530172640e9347a9396849e Mon Sep 17 00:00:00 2001 From: shenlei Date: Mon, 28 Sep 2026 16:56:36 +0900 Subject: [PATCH] native_render: HiDPI glyph pages, sharp UI upscaling, 4x MSAA - CGraphicFontTexture rasterises glyph pages at the swapchain density (UISetFontOversample, MT_FONT_OVERSAMPLE) with anti-aliased coverage; metrics, packing and UVs stay the 40250 1x values. Default 1 keeps the verbatim bilevel path (Godot unchanged). - native.frag: sharp-bilinear sampling for magnified UI textures. - Multisampled colour/depth with a resolve into the swapchain (MT_MSAA, default 4); stats line reports msaa=N. Co-Authored-By: Claude Opus 5.5 --- .../src/platform/EterLib/GrpFontTexture.cpp | 78 +++++++++- .../src/platform/EterLib/UIRenderCommands.h | 7 + native_render/main.cpp | 135 ++++++++++++++---- native_render/native.frag | 15 +- 4 files changed, 200 insertions(+), 35 deletions(-) diff --git a/extension/src/platform/EterLib/GrpFontTexture.cpp b/extension/src/platform/EterLib/GrpFontTexture.cpp index 6b3c0ca8..d298dea1 100644 --- a/extension/src/platform/EterLib/GrpFontTexture.cpp +++ b/extension/src/platform/EterLib/GrpFontTexture.cpp @@ -5,6 +5,38 @@ #include "EterBase/Stl.h" #include "EterLib/Util.h" +#include "UIRenderCommands.h" + +#include + +// PORT: HiDPI glyph pages (UISetFontOversample). With a scale s > 1 the page DIB and texture are +// s times larger and each glyph is drawn with an s-times font into the s-scaled cell, while the +// 40250 metrics (width, height, advance), the packing and therefore the page UVs stay the logical +// 1x values: text layout is unchanged, only the texels get finer. +namespace { +int g_font_oversample = 1; +bool g_font_oversample_latched = false; +// The 1x HFONT a CGraphicFontTexture measures with -> its s-times twin that draws. +std::unordered_map g_oversampled_fonts; + +int FontOversample() +{ + g_font_oversample_latched = true; + return g_font_oversample; +} +} // namespace + +void UISetFontOversample(int scale) +{ + if (g_font_oversample_latched) + return; + g_font_oversample = scale < 1 ? 1 : (scale > 4 ? 4 : scale); +} + +int UIFontOversample() +{ + return g_font_oversample; +} CGraphicFontTexture::CGraphicFontTexture() { @@ -49,6 +81,13 @@ void CGraphicFontTexture::Destroy() while(i != m_fontMap.end()) { + TFontMap::mapped_type hFont = i->second; + auto hi = g_oversampled_fonts.find(hFont); + if (hi != g_oversampled_fonts.end()) + { + DeleteObject((HGDIOBJ)hi->second); + g_oversampled_fonts.erase(hi); + } DeleteObject((HGDIOBJ)i->second); ++i; } @@ -86,7 +125,8 @@ bool CGraphicFontTexture::Create(const char* c_szFontName, int fontSize, bool bI if (GetMaxTextureHeight() > 512) height = 512; - if (!m_dib.Create(ms_hDC, width, height)) + const int s = FontOversample(); + if (!m_dib.Create(ms_hDC, width * s, height * s)) return false; HDC hDC = m_dib.GetDCHandle(); @@ -139,6 +179,13 @@ HFONT CGraphicFontTexture::GetFont(WORD codePage) hFont = CreateFontIndirect(&logFont); m_fontMap.insert(TFontMap::value_type(codePage, hFont)); + + if (const int s = FontOversample(); s > 1 && hFont) + { + logFont.lfHeight = m_fontSize * s; + if (HFONT hHiFont = CreateFontIndirect(&logFont)) + g_oversampled_fonts[hFont] = hHiFont; + } } return hFont; @@ -210,7 +257,8 @@ CGraphicFontTexture::TCharacterInfomation* CGraphicFontTexture::GetCharacterInfo CGraphicFontTexture::TCharacterInfomation* CGraphicFontTexture::UpdateCharacterInfomation(TCharacterKey code) { HDC hDC = m_dib.GetDCHandle(); - SelectObject(hDC, GetFont(code.first)); + HFONT hFont = GetFont(code.first); + SelectObject(hDC, hFont); wchar_t keyValue = code.second; @@ -232,8 +280,9 @@ CGraphicFontTexture::TCharacterInfomation* CGraphicFontTexture::UpdateCharacterI LONG lAdvance = ceilf( stABC.abcfA + stABC.abcfB + stABC.abcfC ); - int width = m_dib.GetWidth(); - int height = m_dib.GetHeight(); + const int s = FontOversample(); + int width = m_dib.GetWidth() / s; + int height = m_dib.GetHeight() / s; if (m_x + size.cx >= (width - 1)) { @@ -255,6 +304,26 @@ CGraphicFontTexture::TCharacterInfomation* CGraphicFontTexture::UpdateCharacterI } } + auto hi = s > 1 ? g_oversampled_fonts.find(hFont) : g_oversampled_fonts.end(); + if (hi != g_oversampled_fonts.end()) + { + // Draw with the s-times font into the s-scaled cell and keep its anti-aliasing as + // A4R4G4B4 white with coverage alpha (1x keeps 40250's bilevel threshold below). + SelectObject(hDC, hi->second); + TextOutW(hDC, m_x * s, m_y * s, &keyValue, 1); + SelectObject(hDC, hFont); + + const int nDIBWidth = m_dib.GetWidth(); + DWORD* pdwRow = (DWORD*)m_dib.GetPointer() + nDIBWidth * (m_y * s) + m_x * s; + for (int y = 0; y < size.cy * s; ++y, pdwRow += nDIBWidth) + for (int x = 0; x < size.cx * s; ++x) + { + const DWORD alpha = ((pdwRow[x] & 0xff) * 15 + 127) / 255; + pdwRow[x] = alpha ? ((alpha << 12) | 0x0fff) : 0; + } + } + else + { TextOutW(hDC, m_x, m_y, &keyValue, 1); int nChrX; @@ -276,6 +345,7 @@ CGraphicFontTexture::TCharacterInfomation* CGraphicFontTexture::UpdateCharacterI pdwDIBRow[nChrX]=(pdwDIBRow[nChrX]&0xff) ? 0xffff : 0; } } + } float rhwidth = 1.0f / float(width); float rhheight = 1.0f / float(height); diff --git a/extension/src/platform/EterLib/UIRenderCommands.h b/extension/src/platform/EterLib/UIRenderCommands.h index 7a712ee5..fa1d245c 100644 --- a/extension/src/platform/EterLib/UIRenderCommands.h +++ b/extension/src/platform/EterLib/UIRenderCommands.h @@ -72,3 +72,10 @@ void UIRenderAdd(UIRenderCommand command); const std::vector& UIRenderCommands(); void UIRenderSetClip(float x, float y, float width, float height); void UIRenderRestoreClip(); + +// HiDPI text: CGraphicFontTexture rasterises its glyph pages at this multiple of the logical font +// size (the swapchain's pixels per UI pixel) while keeping the 40250 glyph metrics and page UVs in +// logical pixels. 1 (the default) is the verbatim 40250 path. Latched when the first font page is +// created, so set it before the game creates fonts. +void UISetFontOversample(int scale); +int UIFontOversample(); diff --git a/native_render/main.cpp b/native_render/main.cpp index 669738a1..cdc74dd3 100644 --- a/native_render/main.cpp +++ b/native_render/main.cpp @@ -942,6 +942,7 @@ public: if (depth_view_) vkDestroyImageView(device_, depth_view_, nullptr); if (depth_image_) vkDestroyImage(device_, depth_image_, nullptr); if (depth_memory_) vkFreeMemory(device_, depth_memory_, nullptr); + destroy_msaa_color(); for (auto view : image_views_) vkDestroyImageView(device_, view, nullptr); if (swapchain_) vkDestroySwapchainKHR(device_, swapchain_, nullptr); if (pool_) vkDestroyCommandPool(device_, pool_, nullptr); @@ -979,6 +980,7 @@ public: screenshot_frame_ = frame; } + int msaa_samples() const { return int(samples_); } std::uint32_t width() const { return extent_.width; } std::uint32_t height() const { return extent_.height; } std::uint32_t logical_width() const { @@ -1491,14 +1493,14 @@ public: vkCmdResetQueryPool(command_, query_pool_, 0, 2); vkCmdWriteTimestamp(command_, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, query_pool_, 0); - VkClearValue clears[2]{}; + 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}; 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 = 2; pass_begin.pClearValues = clears; + pass_begin.renderArea.extent = extent_; pass_begin.clearValueCount = samples_ != VK_SAMPLE_COUNT_1_BIT ? 3 : 2; pass_begin.pClearValues = clears; vkCmdBeginRenderPass(command_, &pass_begin, VK_SUBPASS_CONTENTS_INLINE); VkPipeline current_pipeline = VK_NULL_HANDLE; @@ -1709,19 +1711,12 @@ private: 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(); - for (auto view : image_views_) { - VkImageView fb_attachments[2] = {view, depth_view_}; - VkFramebufferCreateInfo framebuffer{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; - framebuffer.renderPass = pass_; framebuffer.attachmentCount = 2; 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 recreate"); - framebuffers_.push_back(handle); - } + create_framebuffers(); get_pipeline(0, 2, 0); } @@ -1963,6 +1958,16 @@ private: 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 available(extension_count); @@ -2044,7 +2049,7 @@ private: 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 = VK_SAMPLE_COUNT_1_BIT; + 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; @@ -2062,6 +2067,63 @@ private: 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(); + } + + // The multisampled colour target the subpass resolves into the swapchain image. It never + // leaves the render pass, so it is transient (lazily allocated tile memory where available). + void 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 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 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); + } } void create_host_buffer(VkDeviceSize size, VkBufferUsageFlags usage, VkBuffer& buffer, VkDeviceMemory& memory, void** mapped) { @@ -2292,20 +2354,29 @@ private: } void create_render_pass_and_layout() { - VkAttachmentDescription attachments[2]{}; - attachments[0].format = swapchain_format_; attachments[0].samples = VK_SAMPLE_COUNT_1_BIT; - attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; - attachments[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; attachments[0].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; - attachments[1].format = VK_FORMAT_D32_SFLOAT; attachments[1].samples = VK_SAMPLE_COUNT_1_BIT; + 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{}; @@ -2315,20 +2386,11 @@ private: 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 = 2; pass_info.pAttachments = attachments; + 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"); - - for (auto view : image_views_) { - VkImageView fb_attachments[2] = {view, depth_view_}; - VkFramebufferCreateInfo framebuffer{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; - framebuffer.renderPass = pass_; framebuffer.attachmentCount = 2; 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); - } + create_framebuffers(); const auto vert = read_spirv("native.vert.spv"); const auto frag = read_spirv("native.frag.spv"); @@ -2401,7 +2463,7 @@ private: raster.lineWidth = 1; VkPipelineMultisampleStateCreateInfo multisample{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; - multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + multisample.rasterizationSamples = samples_; VkPipelineDepthStencilStateCreateInfo depth_stencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; depth_stencil.depthTestEnable = depth > 0 ? VK_TRUE : VK_FALSE; @@ -2986,6 +3048,10 @@ private: VkImage depth_image_ = VK_NULL_HANDLE; VkDeviceMemory depth_memory_ = VK_NULL_HANDLE; VkImageView depth_view_ = VK_NULL_HANDLE; + VkSampleCountFlagBits samples_ = VK_SAMPLE_COUNT_1_BIT; + VkImage msaa_image_ = VK_NULL_HANDLE; + VkDeviceMemory msaa_memory_ = VK_NULL_HANDLE; + VkImageView msaa_view_ = VK_NULL_HANDLE; VkRenderPass pass_ = VK_NULL_HANDLE; std::vector framebuffers_; VkSampler sampler_ = VK_NULL_HANDLE; @@ -3081,7 +3147,7 @@ void print_summary(const VulkanWindow& renderer, int completed, double wall_ms, return frame_ms[std::min(index, frame_ms.size() - 1)]; }; std::cout << "device=" << renderer.device_name() - << " present_mode=" << renderer.present_mode_name() + << " present_mode=" << renderer.present_mode_name() << " msaa=" << renderer.msaa_samples() << " frames=" << completed << " draws=" << renderer.draw_count() << " skinned_draws=" << renderer.skinned_draw_count() @@ -3175,6 +3241,15 @@ int run_live_client( game_port = server.GamePort(); } + { + // HiDPI text: rasterise glyph pages at the swapchain's pixels per UI pixel (before any font + // exists). MT_FONT_OVERSAMPLE=1 keeps the 40250 bilevel 1x glyphs. + const float density = float(renderer.width()) / float(std::max(1u, renderer.logical_width())); + int oversample = std::clamp(int(std::lround(density)), 1, 4); + if (const char* env = std::getenv("MT_FONT_OVERSAMPLE")) oversample = std::max(1, std::atoi(env)); + UISetFontOversample(oversample); + } + std::string error; const char* env_stdlib = std::getenv("MT_PYTHON_STDLIB"); std::string stdlib = (env_stdlib && *env_stdlib) ? env_stdlib : bundled_file_path("python27.zip"); diff --git a/native_render/native.frag b/native_render/native.frag index c35fb505..6984eade 100644 --- a/native_render/native.frag +++ b/native_render/native.frag @@ -105,7 +105,20 @@ void main() { return; } // 2D UI batch (UIRenderCommand): vertex color times texture, optionally clipped by a mask. - vec4 color = in_color * texture(tex_sampler, in_uv); + // The UI is laid out in logical pixels and magnified onto a HiDPI swapchain. Plain bilinear + // smears each texel over the scale factor; "sharp bilinear" keeps texels square and only blends + // across a one-pixel seam. Minified or 1:1 texels keep the original bilinear lookup. + vec2 uv = in_uv; + vec2 tex_size = vec2(textureSize(tex_sampler, 0)); + vec2 texel = uv * tex_size; + vec2 texels_per_pixel = fwidth(texel); + if (texels_per_pixel.x > 0.0 && texels_per_pixel.y > 0.0 && + texels_per_pixel.x < 0.99 && texels_per_pixel.y < 0.99) { + vec2 seam = floor(texel + 0.5); + texel = seam + clamp((texel - seam) / texels_per_pixel, -0.5, 0.5); + uv = texel / tex_size; + } + vec4 color = in_color * texture(tex_sampler, uv); if (draw.params.z > 0.5) { if (in_mask_uv.x < 0.0 || in_mask_uv.x > 1.0 || in_mask_uv.y < 0.0 || in_mask_uv.y > 1.0) discard;