diff --git a/code/graphics/material.cpp b/code/graphics/material.cpp index cd73632831f..5a57d0359e6 100644 --- a/code/graphics/material.cpp +++ b/code/graphics/material.cpp @@ -184,6 +184,17 @@ void material_set_batched_bitmap(batched_bitmap_material* mat_info, int base_tex material_set_unlit(mat_info, base_tex, alpha, true, true); mat_info->set_color_scale(color_scale); + + // When an additive batched bitmap (e.g. a model glowpoint) is drawn into a render target, its + // texture typically has no alpha channel, so the sampled alpha is 1 everywhere. Additive alpha + // blending then inflates the target's alpha to opaque across the whole sprite quad, even where + // the RGB is ~black. Because these render targets are later straight-alpha composited (e.g. the + // SCPUI 3D ship/weapon select preview), those dark regions show up as opaque black squares. Skip + // alpha writes in that case so the target's coverage (the opaque model silhouette) is preserved. + // Normal screen rendering (rendering_to_texture == -1) is unaffected. + if (gr_screen.rendering_to_texture != -1 && mat_info->get_blend_mode() == ALPHA_BLEND_ADDITIVE) { + mat_info->set_color_mask(true, true, true, false); + } } void material_set_batched_opaque_bitmap(batched_bitmap_material* mat_info, int base_tex, float color_scale) { diff --git a/code/graphics/vulkan/VulkanDeferred.cpp b/code/graphics/vulkan/VulkanDeferred.cpp index be9625fe4c5..0036ce47d6b 100644 --- a/code/graphics/vulkan/VulkanDeferred.cpp +++ b/code/graphics/vulkan/VulkanDeferred.cpp @@ -636,6 +636,10 @@ namespace graphics::vulkan { namespace { bool Glowpoint_override_save = false; bool Restore_swapchain_after_shadow_pass = false; +// Non-null when the shadow pass was started while rendering into an off-screen render target +// (e.g. SCPUI's 3D ship/weapon select preview). In that case the shadow pass must resume the +// render target's pass afterwards, not the swapchain, or subsequent draws leak onto the screen. +tcache_slot_vulkan* Restore_render_target_after_shadow_pass = nullptr; bool Shadow_pass_active = false; } // anonymous namespace @@ -669,6 +673,18 @@ void vulkan_shadow_map_start(matrix4* shadow_view_matrix, const matrix* light_ma // End the current render pass but keep track if we need to resume the swapchain render pass or the scene render pass afterwards. Restore_swapchain_after_shadow_pass = !getRendererInstance()->isSceneRendering(); + // If shadows are being rendered while an off-screen render target is bound (SCPUI 3D + // ship/weapon select preview renders a rotating model via draw_model_rotating into an RTT), + // remember that target so vulkan_shadow_map_end() resumes ITS pass instead of the swapchain. + Restore_render_target_after_shadow_pass = nullptr; + if (getRendererInstance()->isRenderTargetActive()) { + auto* texMgr = getTextureManager(); + const int rtHandle = texMgr ? texMgr->getCurrentRenderTarget() : -1; + if (texMgr && rtHandle >= 0) { + Restore_render_target_after_shadow_pass = texMgr->getTextureSlot(rtHandle); + } + } + // vulkan_build_shadow_tlas() (called just before this, every frame shadows // are enabled) already ends whatever render pass was active -- TLAS builds // are illegal inside a render pass instance -- and nulls out the tracked @@ -743,7 +759,13 @@ void vulkan_shadow_map_end() // End shadow render pass (color transitions to eShaderReadOnlyOptimal via finalLayout) cmd.endRenderPass(); - if ( Restore_swapchain_after_shadow_pass ) { + if ( Restore_render_target_after_shadow_pass ) { + // Shadows were rendered inside an off-screen render target; resume its pass (loadOp=eLoad + // so the pre-shadow contents are preserved) rather than the swapchain, otherwise the + // following model draws would land on the screen at the RTT's viewport (top-left corner). + renderer->resumeRenderTargetPass(Restore_render_target_after_shadow_pass); + Restore_render_target_after_shadow_pass = nullptr; + } else if ( Restore_swapchain_after_shadow_pass ) { renderer->resumeSwapChainPass(); } else if ( renderer->isUsingGbufRenderPass() ) { const bool msaaActive = (Cmdline_msaa_enabled > 0 && pp->deferred().isMsaaInitialized()); diff --git a/code/graphics/vulkan/VulkanTexture.cpp b/code/graphics/vulkan/VulkanTexture.cpp index 48d98f95048..9def86c6b11 100644 --- a/code/graphics/vulkan/VulkanTexture.cpp +++ b/code/graphics/vulkan/VulkanTexture.cpp @@ -1424,11 +1424,40 @@ int VulkanTextureManager::bm_make_render_target(int handle, int* width, int* hei subpass.pDepthStencilAttachment = &depthAttachmentRef; } + // These targets are sampled as textures between passes, and some are re-rendered + // every frame while simultaneously being displayed (e.g. the SCPUI load bar, redrawn + // each frame and shown by the UI). With no explicit dependencies the render pass only + // gets the implicit TOP_OF_PIPE->BOTTOM_OF_PIPE dependency, which orders the layout + // transitions but creates no memory dependency between the color writes here and the + // fragment-shader sample in a later pass. That leaves a read-after-write (and, on + // re-render, write-after-read) hazard that manifests as flickering/garbage. Add + // both-direction external dependencies against the shader-read use to close it. + std::array dependencies{}; + // Pass start: a prior fragment-shader sample of this target must complete before we + // overwrite the color attachment (write-after-read; also covers loadOp=eLoad reads). + dependencies[0].srcSubpass = VK_SUBPASS_EXTERNAL; + dependencies[0].dstSubpass = 0; + dependencies[0].srcStageMask = vk::PipelineStageFlagBits::eFragmentShader; + dependencies[0].srcAccessMask = vk::AccessFlagBits::eShaderRead; + dependencies[0].dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput; + dependencies[0].dstAccessMask = + vk::AccessFlagBits::eColorAttachmentWrite | vk::AccessFlagBits::eColorAttachmentRead; + // Pass end: the color writes must be available and visible before the next + // fragment-shader sample of this target (read-after-write). + dependencies[1].srcSubpass = 0; + dependencies[1].dstSubpass = VK_SUBPASS_EXTERNAL; + dependencies[1].srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput; + dependencies[1].srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; + dependencies[1].dstStageMask = vk::PipelineStageFlagBits::eFragmentShader; + dependencies[1].dstAccessMask = vk::AccessFlagBits::eShaderRead; + vk::RenderPassCreateInfo renderPassInfo; renderPassInfo.attachmentCount = hasDepth ? 2u : 1u; renderPassInfo.pAttachments = attachments.data(); renderPassInfo.subpassCount = 1; renderPassInfo.pSubpasses = &subpass; + renderPassInfo.dependencyCount = static_cast(dependencies.size()); + renderPassInfo.pDependencies = dependencies.data(); try { ts->renderPass = m_device.createRenderPass(renderPassInfo); @@ -1463,6 +1492,8 @@ int VulkanTextureManager::bm_make_render_target(int handle, int* width, int* hei loadPassInfo.pAttachments = loadAttachments.data(); loadPassInfo.subpassCount = 1; loadPassInfo.pSubpasses = &subpass; + loadPassInfo.dependencyCount = static_cast(dependencies.size()); + loadPassInfo.pDependencies = dependencies.data(); try { ts->renderPassLoad = m_device.createRenderPass(loadPassInfo); diff --git a/code/graphics/vulkan/gr_vulkan.cpp b/code/graphics/vulkan/gr_vulkan.cpp index d9a15610869..4d5d5768ff8 100644 --- a/code/graphics/vulkan/gr_vulkan.cpp +++ b/code/graphics/vulkan/gr_vulkan.cpp @@ -363,7 +363,13 @@ SCP_string vulkan_blob_screen() } auto* ts = texManager->getTextureSlot(rtHandle); if (ts && renderer_instance->readbackRenderTarget(ts, &pixels, &w, &h)) { - SCP_string result = png_b64_bitmap(w, h, false, pixels); + // Flip vertically to match OpenGL's gr_opengl_blob_screen (which also passes y_flip=true). + // OpenGL render targets are stored bottom-up, so its readback flips to produce an upright + // PNG; Vulkan stores top-down but reads row 0 first, so the same flip is required to yield + // the SAME orientation OpenGL produces. Mods drive screenToBlob assuming OpenGL behavior + // (e.g. SCPUI deliberately draws its source icons V-flipped into the target to cancel this + // flip), so matching OpenGL here keeps that content correct instead of upside-down. + SCP_string result = png_b64_bitmap(w, h, true, pixels); vm_free(pixels); return "data:image/png;base64," + result; } diff --git a/code/scripting/api/libs/graphics.cpp b/code/scripting/api/libs/graphics.cpp index 7b6e41060cb..024ea9c009f 100644 --- a/code/scripting/api/libs/graphics.cpp +++ b/code/scripting/api/libs/graphics.cpp @@ -2409,18 +2409,25 @@ ADE_FUNC(freeAllModels, l_Graphics, nullptr, "Releases all loaded models and fre ADE_FUNC(createColor, l_Graphics, "number Red, number Green, number Blue, [number Alpha]", - "Creates a color object. Values are capped 0-255. Alpha defaults to 255.", + "Creates a color object. Values are capped 0-255. Alpha may be given either as 0-255 or as a " + "0-1 fraction (a value strictly between 0 and 1 is treated as a fraction and scaled up); it defaults to 255.", "color", "The color") { int r; int g; int b; - int a = 255; - if (!ade_get_args(L, "iii|i", &r, &g, &b, &a)) { + // Read alpha as a float so callers can pass either the historical 0-255 value or a 0-1 fraction. + float a_in = 255.0f; + if (!ade_get_args(L, "iii|f", &r, &g, &b, &a_in)) { return ADE_RETURN_NIL; } + // A value strictly between 0 and 1 can only be a fraction: as an integer it would previously have + // been truncated to 0 (never rendering), so scaling it to 0-255 only fixes that broken case and + // leaves every existing 0-255 integer usage untouched. + int a = (a_in > 0.0f && a_in < 1.0f) ? static_cast(a_in * 255.0f + 0.5f) : static_cast(a_in); + CLAMP(r, 0, 255); CLAMP(g, 0, 255); CLAMP(b, 0, 255); diff --git a/freespace2/freespace.cpp b/freespace2/freespace.cpp index aa34d05d612..58b33f172a8 100644 --- a/freespace2/freespace.cpp +++ b/freespace2/freespace.cpp @@ -6782,8 +6782,7 @@ void game_spew_pof_info() if(out == nullptr){ BAIL(); } - int counted = 0; - for(int idx=0; idx= 0){