Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions code/graphics/material.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
24 changes: 23 additions & 1 deletion code/graphics/vulkan/VulkanDeferred.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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());
Expand Down
31 changes: 31 additions & 0 deletions code/graphics/vulkan/VulkanTexture.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<vk::SubpassDependency, 2> 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<uint32_t>(dependencies.size());
renderPassInfo.pDependencies = dependencies.data();

try {
ts->renderPass = m_device.createRenderPass(renderPassInfo);
Expand Down Expand Up @@ -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<uint32_t>(dependencies.size());
loadPassInfo.pDependencies = dependencies.data();

try {
ts->renderPassLoad = m_device.createRenderPass(loadPassInfo);
Expand Down
8 changes: 7 additions & 1 deletion code/graphics/vulkan/gr_vulkan.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
13 changes: 10 additions & 3 deletions code/scripting/api/libs/graphics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>(a_in * 255.0f + 0.5f) : static_cast<int>(a_in);

CLAMP(r, 0, 255);
CLAMP(g, 0, 255);
CLAMP(b, 0, 255);
Expand Down
3 changes: 1 addition & 2 deletions freespace2/freespace.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6782,8 +6782,7 @@ void game_spew_pof_info()
if(out == nullptr){
BAIL();
}
int counted = 0;
for(int idx=0; idx<num_files; idx++, counted++){
for(int idx=0; idx<num_files; idx++){
sprintf(str, "%s.pof", pof_list[idx]);
int model_num = model_load(str);
if(model_num >= 0){
Expand Down
Loading