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
1 change: 1 addition & 0 deletions docs/dev/release-completion.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
45 changes: 43 additions & 2 deletions src/renderer/Vulkan/vk_GuiExecutor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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, &region, VK_FILTER_NEAREST );

destination->everUploaded = true;
Expand Down
102 changes: 102 additions & 0 deletions src/renderer/Vulkan/vk_Image.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 );
}
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/Vulkan/vk_Image.h
Original file line number Diff line number Diff line change
Expand Up @@ -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__ */