diff --git a/docs/dev/release-completion.md b/docs/dev/release-completion.md index a4cdcaa51..de07812a0 100644 --- a/docs/dev/release-completion.md +++ b/docs/dev/release-completion.md @@ -77,6 +77,7 @@ is `docs/dev/macos-moltenvk-decision.md`. ## Ready For Changelog +- [x] Heat haze and the other `_currentRender` effects survive antialiasing on the Vulkan backend. With `r_multiSamples` above 0 the scene renders into a multisampled target, `vkCmdBlitImage` cannot read one, and the `_currentRender` capture failed outright; every surface that consumes it — the fire distortion in `effects/fire/*.fx`, the `warp_mask` stages, heat haze — was then skipped for the whole frame, so multisampling silently deleted those effects. The capture now resolves a multisampled colour source into single-sample scratch of the source's own format and blits from that, using the same `vkCmdResolveImage` path the render-target resolve already uses. Depth capture is unchanged; a multisampled depth source is still refused. The added resolve costs roughly 1 ms at 2560x1440 with 4x multisampling on Apple M-class and A19-class GPUs. - [x] The view weapon kicks back when it fires in multiplayer, as it always has in single player, and a new **Settings > Game Options > View Weapon > Weapon Kick** option (`g_weaponMuzzleKick`, on by default) turns it off in both. The engine code was never the difference: `idGameLocal::FindEntityDef` redirects every multiplayer lookup from `X` to `X_mp`, and every stock `weapon_*_mp` def zeroes the four `muzzle_kick_*` keys, so `rvWeapon::MuzzleRise` returned at its `muzzle_kick_maxtime <= 0` guard. Each multiplayer weapon now recovers its own single-player kick from the def that redirect replaced. This is presentation only: the kick feeds the view model transform, and no stock weapon sets `launchFromBarrel`, so aim and shot origin cannot follow it. The separate `recoilAngles` view punch those defs also zero is deliberately left off. - [x] #158 navigation: openQ4 now compiles Quake 4 AAS in the engine. `dmap` rebuilds every AAS type declared by `aas_types` after writing `.proc`/`.cm`, `runAAS`, `runAASDir` and `runReach` register alongside `dmap` instead of inside `ID_ALLOW_TOOLS`, and unused AAS types get the placeholder file the runtime recognises as a dummy. Compiled `game/core2` reproduces the retail file's geometry CRC (3455855974) and navigation bounds exactly, with 1168 areas against retail's 1091 and 8435 valid reachabilities against 7566; forcing `-playerFlood` reproduces the shipped `aas48` bounds as well. The map loads all five compiled files with no AAS warnings, and a full `dmap mp/liquid_lab` produces `.proc`, `.cm` and five AAS files. Two defects found on the way: `idAASFileLocal::Optimize()` wrote uninitialised allocator memory as the dummy edge/face of every optimized AAS file, and `idVectorSet::FindVector()` never compiled because its base-class `Append` was unqualified. Raven's tactical feature lump is still not generated, so recompiled maps keep full pathing but lose navigation-mesh cover and lean positions. See [AAS compiler](aas-compiler.md). - [x] Compiled map output now reports the absolute path of each file it writes. `dmap` writes `.proc`/`.aas*` under `fs_cdpath` and `.cm` under `fs_devpath` (unset by default, so it falls back to `fs_savepath`), which are different directories from each other and from the `.map` that was read; the reporter on #158 concluded no `.proc` had been produced when it had. diff --git a/src/renderer/Vulkan/vk_GuiExecutor.cpp b/src/renderer/Vulkan/vk_GuiExecutor.cpp index d2842e5e9..2f7b37b4f 100644 --- a/src/renderer/Vulkan/vk_GuiExecutor.cpp +++ b/src/renderer/Vulkan/vk_GuiExecutor.cpp @@ -4772,10 +4772,14 @@ bool VK_Exec_CopyRender( idImage *image, int x, int y, int width, int height, sourceImage = sourceEntry != NULL ? sourceEntry->image : vkCtx.swapchainImages[ vkExec.swapImageIndex ]; sourceFormat = sourceEntry != NULL ? sourceEntry->format : vkCtx.swapchainFormat; } + // A multisampled colour source is resolved below. A multisampled depth + // source is still refused: that needs VK_KHR_depth_stencil_resolve inside a + // render pass, and no shipped content captures depth from an MSAA target. if ( sourceImage == VK_NULL_HANDLE || ( sourceEntry == NULL && !copyDepth && !vkCtx.swapchainTransferSrc ) || ( sourceEntry != NULL && ( sourceEntry->usage & VK_IMAGE_USAGE_TRANSFER_SRC_BIT ) == 0 ) - || ( sourceEntry != NULL && sourceEntry->samples != VK_SAMPLE_COUNT_1_BIT ) ) { + || ( sourceEntry != NULL && copyDepth + && sourceEntry->samples != VK_SAMPLE_COUNT_1_BIT ) ) { return false; } @@ -4837,6 +4841,43 @@ bool VK_Exec_CopyRender( idImage *image, int x, int y, int width, int height, } VK_Exec_TransitionImage( destination, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL ); + // With r_multiSamples on, the scene target is multisampled and + // vkCmdBlitImage refuses it outright, so the capture failed here and + // VK_GuiExecutor_Draw3DView then skipped every _currentRender-consuming + // surface. Resolve into single-sample scratch of the source's own format + // first; the format-converting, Y-flipping blit below then runs exactly as + // it does at one sample (a resolve can neither flip nor convert, so both + // steps are needed). + VkImage blitSource = sourceImage; + if ( !copyDepth && sourceEntry != NULL + && sourceEntry->samples != VK_SAMPLE_COUNT_1_BIT ) { + vkImageEntry_t *scratch = VK_Image_AcquireResolveScratch( sourceWidth, + sourceHeight, sourceFormat ); + if ( scratch == NULL ) { + VK_Exec_TransitionImage( destination, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL ); + VK_Exec_TransitionImage( sourceEntry, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL ); + VK_Exec_BeginMainRendering( false ); + return false; + } + VK_Exec_TransitionImage( scratch, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL ); + + VkImageResolve resolveRegion; + memset( &resolveRegion, 0, sizeof( resolveRegion ) ); + resolveRegion.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + resolveRegion.srcSubresource.layerCount = 1; + resolveRegion.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + resolveRegion.dstSubresource.layerCount = 1; + resolveRegion.extent.width = (uint32_t)sourceWidth; + resolveRegion.extent.height = (uint32_t)sourceHeight; + resolveRegion.extent.depth = 1; + vkCmdResolveImage( vkExec.cmd, sourceImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + scratch->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &resolveRegion ); + + scratch->everUploaded = true; + VK_Exec_TransitionImage( scratch, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL ); + blitSource = scratch->image; + } + if ( copyDepth ) { if ( VK_Exec_DepthBlitSupported( sourceFormat ) ) { // One flipped NEAREST blit reproduces the per-row copy's GL @@ -4924,7 +4965,7 @@ bool VK_Exec_CopyRender( idImage *image, int x, int y, int width, int height, region.dstOffsets[ 1 ].x = width; region.dstOffsets[ 1 ].y = height; region.dstOffsets[ 1 ].z = 1; - vkCmdBlitImage( vkExec.cmd, sourceImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + vkCmdBlitImage( vkExec.cmd, blitSource, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion, VK_FILTER_NEAREST ); destination->everUploaded = true; diff --git a/src/renderer/Vulkan/vk_Image.cpp b/src/renderer/Vulkan/vk_Image.cpp index 7a6572bd9..4a83c5642 100644 --- a/src/renderer/Vulkan/vk_Image.cpp +++ b/src/renderer/Vulkan/vk_Image.cpp @@ -326,6 +326,107 @@ static VkSampler VK_Image_GetSampler( textureFilter_t filter, textureRepeat_t re return sampler; } +/* +==================== +VK_Image_AcquireResolveScratch + +vkCmdBlitImage cannot read a multisampled source, so a _currentRender capture +taken while the scene renders into an MSAA target has to be resolved first. +vkCmdResolveImage demands a destination of the source's exact format with one +sample, while the capture destination is the front-end's own format, so the +resolve needs its own storage: this image. It is also the blit's source, hence +TRANSFER_SRC as well as TRANSFER_DST. One image, reused every frame, recreated +through the deferred-destroy queue when the render target changes size or +format, and never entered in the image table because it is never sampled. +==================== +*/ +static vkImageEntry_t vkResolveScratch; + +static void VK_Image_ReleaseResolveScratch( bool deferred ) { + if ( vkResolveScratch.image != VK_NULL_HANDLE ) { + if ( deferred ) { + // an in-flight frame may still be reading it + VK_Device_DeferDestroy( vkResolveScratch.image, VK_NULL_HANDLE, + VK_NULL_HANDLE, vkResolveScratch.allocation ); + } else if ( vkCtx.allocator != NULL ) { + vmaDestroyImage( vkCtx.allocator, vkResolveScratch.image, + vkResolveScratch.allocation ); + } + } + memset( &vkResolveScratch, 0, sizeof( vkResolveScratch ) ); +} + +vkImageEntry_t *VK_Image_AcquireResolveScratch( int width, int height, + VkFormat format ) { + if ( !vkCtx.initialized || width <= 0 || height <= 0 + || format == VK_FORMAT_UNDEFINED ) { + return NULL; + } + if ( vkResolveScratch.image != VK_NULL_HANDLE + && vkResolveScratch.width == width + && vkResolveScratch.height == height + && vkResolveScratch.format == format ) { + return &vkResolveScratch; + } + VK_Image_ReleaseResolveScratch( true ); + + const VkImageUsageFlags usage = + VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + + VkImageCreateInfo ici; + memset( &ici, 0, sizeof( ici ) ); + ici.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + ici.imageType = VK_IMAGE_TYPE_2D; + ici.format = format; + ici.extent.width = (uint32_t)width; + ici.extent.height = (uint32_t)height; + ici.extent.depth = 1; + ici.mipLevels = 1; + ici.arrayLayers = 1; + ici.samples = VK_SAMPLE_COUNT_1_BIT; + ici.tiling = VK_IMAGE_TILING_OPTIMAL; + ici.usage = usage; + ici.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + VmaAllocationCreateInfo vaci; + memset( &vaci, 0, sizeof( vaci ) ); + vaci.usage = VMA_MEMORY_USAGE_AUTO; + + VkImage newImage = VK_NULL_HANDLE; + VmaAllocation newAllocation = NULL; + if ( vmaCreateImage( vkCtx.allocator, &ici, &vaci, + &newImage, &newAllocation, NULL ) != VK_SUCCESS ) { + static bool warnedCreate = false; + if ( !warnedCreate ) { + warnedCreate = true; + common->Warning( "Vulkan: capture resolve scratch creation failed (%dx%d fmt %d)", + width, height, (int)format ); + } + memset( &vkResolveScratch, 0, sizeof( vkResolveScratch ) ); + return NULL; + } + + vkResolveScratch.image = newImage; + vkResolveScratch.allocation = newAllocation; + vkResolveScratch.view = VK_NULL_HANDLE; + vkResolveScratch.attachmentView = VK_NULL_HANDLE; + vkResolveScratch.sampler = VK_NULL_HANDLE; + vkResolveScratch.format = format; + vkResolveScratch.usage = usage; + vkResolveScratch.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + vkResolveScratch.layout = VK_IMAGE_LAYOUT_UNDEFINED; + vkResolveScratch.samples = VK_SAMPLE_COUNT_1_BIT; + vkResolveScratch.width = width; + vkResolveScratch.height = height; + vkResolveScratch.numMips = 1; + vkResolveScratch.numLayers = 1; + vkResolveScratch.isCube = false; + vkResolveScratch.everUploaded = false; + vkResolveScratch.inUse = false; + vkResolveScratch.generation = 0; + return &vkResolveScratch; +} + /* ==================== VK_Image_ShutdownAll @@ -351,6 +452,7 @@ void VK_Image_ShutdownAll( void ) { } memset( &vkImages[ i ], 0, sizeof( vkImages[ i ] ) ); } + VK_Image_ReleaseResolveScratch( false ); for ( int i = 0; i < vkNumSamplers; i++ ) { vkDestroySampler( vkCtx.device, vkSamplers[ i ], NULL ); } diff --git a/src/renderer/Vulkan/vk_Image.h b/src/renderer/Vulkan/vk_Image.h index e08f8d599..9d4dc85a7 100644 --- a/src/renderer/Vulkan/vk_Image.h +++ b/src/renderer/Vulkan/vk_Image.h @@ -51,6 +51,10 @@ vkImageEntry_t *VK_Image_GetEntry( unsigned int texnum ); // dimensions in sync with the copied region. bool VK_Image_MakeDepthCopyTarget( idImage *image, int width, int height, VkFormat depthFormat ); +// Single-sample scratch storage in the source's exact format, used to resolve +// a multisampled scene target before the format-converting feedback blit. +vkImageEntry_t *VK_Image_AcquireResolveScratch( int width, int height, + VkFormat format ); void VK_Image_ShutdownAll( void ); #endif /* !__VK_IMAGE_H__ */