sharpemu/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.DepthT...

288 lines
16 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE.GpuMemory;
using SharpEmu.Libs.Gpu.Buffers;
using SharpEmu.Libs.Gpu.Scheduling;
using Silk.NET.Vulkan;
namespace SharpEmu.Libs.Gpu.Images;
// A resolved depth target: the request plus the planes, sizes and clear enables of the attachment.
public readonly record struct DepthTargetResolution(
ImageRequest Request,
Format Format,
uint Width,
uint Height,
uint Samples,
bool HasStencil,
bool HasHtile,
ulong DepthAddress,
ulong DepthSize,
ulong StencilAddress,
ulong StencilSize,
ulong HtileAddress,
ulong HtileSize,
bool DepthClearEnabled,
bool StencilClearEnabled,
bool DepthWriteDisabled,
bool StencilWriteDisabled);
public static partial class ImageRequestBuilders
{
public static ImageUsageFlags DepthTargetUsage =>
ImageUsageFlags.DepthStencilAttachmentBit | ImageUsageFlags.SampledBit | ImageUsageFlags.TransferSrcBit | ImageUsageFlags.TransferDstBit;
// The first host format of the rule the device supports at the sample count, or Undefined.
private static Format HostDepthAttachmentFormat(IImageFormatSupport device, DepthFormatRule rule, bool hasStencil, uint samples)
{
var required = ImageDescription.VulkanSampleCount(samples);
bool Supports(Format format) =>
format != Format.Undefined &&
device.TryGetImageFormatProperties(format, ImageType.Type2D, ImageTiling.Optimal, DepthTargetUsage, 0, out var properties) &&
(properties.SampleCounts & required) != 0;
if (!hasStencil)
{
return Supports(rule.DepthAttachmentFormat) ? rule.DepthAttachmentFormat : Format.Undefined;
}
foreach (var format in rule.StencilAttachmentFormats)
{
if (Supports(format))
{
return format;
}
}
return Format.Undefined;
}
// Builds the request for the bound depth target. Null when no depth or stencil state is active.
public static DepthTargetResolution? DepthTarget(in DepthTargetWords depthWords, IImageFormatSupport device) =>
DepthTargetCore(in depthWords, device, copyMode: false, writeBuffer: false);
// Builds the source or destination image used by DB_RENDER_OVERRIDE depth/stencil copies.
public static DepthTargetResolution? DepthTargetCopy(in DepthTargetWords depthWords, IImageFormatSupport device, bool writeBuffer) =>
DepthTargetCore(in depthWords, device, copyMode: true, writeBuffer);
private static DepthTargetResolution? DepthTargetCore(in DepthTargetWords depthWords, IImageFormatSupport device, bool copyMode, bool writeBuffer)
{
var hasStencil = depthWords.StencilFormat != GuestStencilFormat.Invalid;
if (!copyMode)
{
var depthActive = depthWords.DepthTestEnabled || depthWords.DepthWriteEnabled || depthWords.DepthBoundsEnabled || depthWords.DepthClearEnabled || depthWords.CopyDepthToColor;
var stencilActive = hasStencil && (depthWords.StencilTestEnabled || depthWords.StencilClearEnabled || depthWords.CopyStencilToColor);
if (!depthActive && !stencilActive)
{
if (Rendering.RenderTrace.Enabled)
{
Rendering.RenderTrace.Write("DepthAttachmentRejected reason=depth-and-stencil-inactive");
}
return null;
}
}
var depthAddress = copyMode && writeBuffer ? depthWords.ZWriteBase : depthWords.ZReadBase;
var stencilAddress = copyMode && writeBuffer ? depthWords.StencilWriteBase : depthWords.StencilReadBase;
// The size register is independent state; a zero encoding alone must not produce an attachment.
var attachmentUnbound =
depthWords.DepthFormat == GuestDepthFormat.Invalid && !hasStencil && depthWords.SamplesLog2 == 0 && !depthWords.ZExpClear && !depthWords.ZPartiallyResident && depthWords.MaxMip == 0 &&
!depthWords.StencilExpClear && !depthWords.StencilPartiallyResident && depthWords.SliceStart == 0 && depthWords.SliceMax == 0 && depthWords.ViewMipLevel == 0 &&
!depthWords.DepthWriteDisabled && !depthWords.StencilWriteDisabled && depthWords.ZReadBase == 0 && depthWords.ZWriteBase == 0 && depthWords.StencilReadBase == 0 &&
depthWords.StencilWriteBase == 0 && depthWords.HtileBase == 0 && !depthWords.HtileAcceleration && depthWords.ShadingRateEncoding == 0 && depthWords.XMax == 0 && depthWords.YMax == 0;
if (attachmentUnbound)
{
if (Rendering.RenderTrace.Enabled)
{
Rendering.RenderTrace.Write("DepthAttachmentRejected reason=unbound-register-state");
}
return null;
}
var hasHtile = depthWords.HtileAcceleration;
var unsupportedShadingRate = depthWords.ShadingRateEncoding > 1 || (depthWords.ShadingRateEncoding != 0 && !hasHtile);
var samples = SampleCount(depthWords.SamplesLog2);
if (samples == 0)
{
throw SubmissionScheduler.Fatal($"The depth fragment count is not supported: samples={depthWords.SamplesLog2}.");
}
if (!TargetViewRange.TryResolve(depthWords.SliceStart, depthWords.SliceMax, 0, out var view))
{
throw SubmissionScheduler.Fatal($"The depth view is invalid: base={depthWords.SliceStart} last={depthWords.SliceMax}.");
}
if (depthWords.CopyDepthToColor || depthWords.CopyStencilToColor || depthWords.CopyCentroid || depthWords.CopySample != 0 || depthWords.ZExpClear || depthWords.StencilExpClear ||
depthWords.ZPartiallyResident || depthWords.StencilPartiallyResident || depthWords.MaxMip != 0 || depthWords.ViewMipLevel != 0 || unsupportedShadingRate ||
depthAddress == 0 || (!copyMode && !depthWords.DepthWriteDisabled && depthWords.ZWriteBase != depthWords.ZReadBase) || (depthAddress & 0xFFFF) != 0 || depthWords.DepthCompare > 7)
{
throw SubmissionScheduler.Fatal(
$"The depth register state is not supported: zInfo=0x{depthWords.ZInfo:X8} stencilInfo=0x{depthWords.StencilInfo:X8} view=0x{depthWords.DepthView:X8} " +
$"renderControl=0x{depthWords.RenderControl:X8} shadingRate={depthWords.ShadingRateEncoding} read=0x{depthWords.ZReadBase:X16} write=0x{depthWords.ZWriteBase:X16}.");
}
if (hasStencil)
{
if (depthWords.StencilFormat != GuestStencilFormat.Stencil8UInt ||
stencilAddress == 0 || (!copyMode && !depthWords.StencilWriteDisabled && depthWords.StencilWriteBase != depthWords.StencilReadBase) || (stencilAddress & 0xFFFF) != 0)
{
throw SubmissionScheduler.Fatal(
$"The stencil attachment state is not supported: stencilInfo=0x{depthWords.StencilInfo:X8} htile={hasHtile} read=0x{depthWords.StencilReadBase:X16} write=0x{depthWords.StencilWriteBase:X16}.");
}
}
else if (!copyMode && (depthWords.StencilReadBase != 0 || depthWords.StencilWriteBase != 0))
{
throw SubmissionScheduler.Fatal($"Stencil state is set without an active stencil attachment: read=0x{depthWords.StencilReadBase:X16} write=0x{depthWords.StencilWriteBase:X16}.");
}
if (hasHtile)
{
if (depthWords.HtileBase == 0 || (depthWords.HtileBase & 0x7FFF) != 0)
{
throw SubmissionScheduler.Fatal($"The HTile metadata address is invalid: htile=0x{depthWords.HtileBase:X16}.");
}
if (depthWords.SliceMax >= 32)
{
throw SubmissionScheduler.Fatal($"HTile clear tracking supports at most 32 slices: last={depthWords.SliceMax}.");
}
}
if (!depthWords.DepthSizeValid)
{
throw SubmissionScheduler.Fatal("The depth extent is missing.");
}
var width = depthWords.XMax + 1;
var height = depthWords.YMax + 1;
if (width > 16384 || height > 16384)
{
throw SubmissionScheduler.Fatal($"The depth extent is invalid: extent={width}x{height}.");
}
var rule = DepthFormatRule.Find(depthWords.DepthFormat) ?? throw SubmissionScheduler.Fatal($"The depth/stencil format pair is not supported: zFormat={(uint)depthWords.DepthFormat} stencil={hasStencil}.");
var format = HostDepthAttachmentFormat(device, rule, hasStencil, samples);
if (format == Format.Undefined)
{
throw SubmissionScheduler.Fatal($"No host depth/stencil format supports the required usage: ideal={(int)rule.AttachmentFormat(hasStencil)} samples={samples}.");
}
var bytes = rule.BytesPerElement;
var pitch = TileGeometry.DepthPitch(width, bytes, depthWords.SamplesLog2);
if (!TileGeometry.TryGetDepthSize(width, height, 0, depthWords.DepthFormat, depthWords.StencilFormat, hasHtile, out var stencilSize, out var htileSize, out var depthSize, depthWords.SamplesLog2) ||
depthSize.Align != 65536 || depthSize.Size == 0 ||
hasStencil != (stencilSize.Align == 65536 && stencilSize.Size != 0) ||
hasHtile != (htileSize.Align == 32768 && htileSize.Size != 0))
{
throw SubmissionScheduler.Fatal(
$"The depth/stencil/HTile footprint is not supported: extent={width}x{height} zFormat={(uint)depthWords.DepthFormat} stencil={hasStencil} htile={hasHtile} samplesLog2={depthWords.SamplesLog2}.");
}
if (depthSize.Size > ulong.MaxValue / view.ImageLayers || stencilSize.Size > ulong.MaxValue / view.ImageLayers || htileSize.Size > ulong.MaxValue / view.ImageLayers)
{
throw SubmissionScheduler.Fatal($"The layered depth footprint overflows: layers={view.ImageLayers}.");
}
var depthBackingSize = (ulong)depthSize.Size * view.ImageLayers;
var stencilBackingSize = (ulong)stencilSize.Size * view.ImageLayers;
var htileBackingSize = (ulong)htileSize.Size * view.ImageLayers;
if (!new GuestSpan(depthAddress, depthBackingSize).IsValid ||
(hasStencil && !new GuestSpan(stencilAddress, stencilBackingSize).IsValid) ||
(hasHtile && !new GuestSpan(depthWords.HtileBase, htileBackingSize).IsValid))
{
throw SubmissionScheduler.Fatal(
$"The layered depth backing range is invalid: depth=0x{depthAddress:X16}+0x{depthBackingSize:X} stencil=0x{stencilAddress:X16}+0x{stencilBackingSize:X} htile=0x{depthWords.HtileBase:X16}+0x{htileBackingSize:X}.");
}
stencilAddress = hasStencil ? stencilAddress : 0;
stencilBackingSize = hasStencil ? stencilBackingSize : 0;
var htileAddress = hasHtile ? depthWords.HtileBase : 0;
htileBackingSize = hasHtile ? htileBackingSize : 0;
var description = ImageDescription.Create();
description.Data = new GuestSpan(depthAddress, depthBackingSize);
description.Stencil = new GuestSpan(stencilAddress, stencilBackingSize);
description.PixelFormat = format;
description.GuestFormat = rule.GuestFormat;
description.Type = GuestImageType.Color2D;
description.Extent = new Extent3D(width, height, 1);
description.Resources = new SubresourceCount(1, view.ImageLayers);
description.Pitch = pitch;
description.BytesPerBlock = bytes;
description.Samples = samples;
description.TileMode = GuestTileMode.Depth;
description.MipLayout[0] = new MipLevelLayout { Offset = 0, Size = depthBackingSize, Pitch = pitch, Height = height };
description.Metadata.Range = new GuestSpan(htileAddress, htileBackingSize);
description.Metadata.Kind = hasHtile ? MetadataKind.Htile : MetadataKind.None;
description.Metadata.StencilCompressed = hasStencil && hasHtile && !depthWords.HtileStencilDisabled;
var viewDescription = new ImageViewDescription(
format, view.LayerCount == 1 ? ImageViewType.Type2D : ImageViewType.Type2DArray, ViewFormatRules.DepthAspects(format),
0, 1, view.BaseLayer, view.LayerCount, default, ImageUsageFlags.DepthStencilAttachmentBit);
var request = new ImageRequest(description, viewDescription, ImageRole.DepthTarget);
return new DepthTargetResolution(
request, format, width, height, samples, hasStencil, hasHtile,
depthAddress, depthBackingSize, stencilAddress, stencilBackingSize, htileAddress, htileBackingSize,
depthWords.DepthClearEnabled, hasStencil && depthWords.StencilClearEnabled && !depthWords.StencilWriteDisabled, depthWords.DepthWriteDisabled, depthWords.StencilWriteDisabled);
}
// Builds the request for a video-out surface registered by the guest.
public static ImageRequest DisplaySurface(in DisplaySurfaceWords surface)
{
const ulong SupportedOptions = (1UL << 3) | (1UL << 5); // Strict colorimetry and premultiplied alpha.
var compression = ImageDescription.ClassifyDisplayCompression(surface.Compressed, surface.MetadataAddress, surface.DccControl, surface.DccClearColor);
if (surface.Width == 0 || surface.Height == 0 || surface.Width > 16384 || surface.Height > 16384 ||
(surface.Option & ~SupportedOptions) != 0 || surface.TilingMode != 0 || surface.DataAddress == 0 ||
compression == DisplayCompression.Unsupported)
{
throw SubmissionScheduler.Fatal(
$"The video-out surface attributes are not supported: address=0x{surface.DataAddress:X16} extent={surface.Width}x{surface.Height} tiling={surface.TilingMode} " +
$"option=0x{surface.Option:X} compressed={surface.Compressed} metadata=0x{surface.MetadataAddress:X16} dccControl=0x{surface.DccControl:X8}.");
}
if (!DisplayFormatRule.TryDecode(surface.PixelFormat, out var pixelFormat))
{
throw SubmissionScheduler.Fatal($"The video-out pixel format is not supported: format=0x{surface.PixelFormat:X16}.");
}
const GuestTileMode tileMode = GuestTileMode.RenderTarget;
var pitch = TileGeometry.TexturePitch(pixelFormat.GuestFormat, surface.Width, tileMode);
var total = TileGeometry.TextureTotalSize(pixelFormat.GuestFormat, surface.Width, surface.Height, 1, 1, tileMode, false);
if (total.Size == 0 || total.Align != 65536 || (surface.DataAddress & (total.Align - 1UL)) != 0)
{
throw SubmissionScheduler.Fatal($"The video-out surface footprint or alignment is invalid: address=0x{surface.DataAddress:X16} size=0x{total.Size:X} align=0x{total.Align:X}.");
}
var description = ImageDescription.Create();
description.Data = new GuestSpan(surface.DataAddress, total.Size);
description.PixelFormat = pixelFormat.HostFormat;
description.GuestFormat = pixelFormat.GuestFormat;
description.Type = GuestImageType.Color2D;
description.Extent = new Extent3D(surface.Width, surface.Height, 1);
description.Resources = SubresourceCount.Single;
description.Pitch = pitch;
description.BytesPerBlock = pixelFormat.BytesPerElement;
description.Samples = 1;
description.TileMode = tileMode;
description.Bgra16 = pixelFormat.Bgra16;
description.MipLayout[0] = new MipLevelLayout { Offset = 0, Size = total.Size, Pitch = pitch, Height = surface.Height };
if (compression != DisplayCompression.Uncompressed)
{
description.Metadata.Range = new GuestSpan(surface.MetadataAddress, 0);
description.Metadata.Kind = MetadataKind.Dcc;
description.Metadata.Control = surface.DccControl;
description.Metadata.Compression = compression;
}
description.Validate();
if (!DisplayFormatRule.Supports(description))
{
throw SubmissionScheduler.Fatal($"The normalized video-out format is not supported: format={(int)description.PixelFormat} guestFormat={(uint)description.GuestFormat}.");
}
var view = new ImageViewDescription(
description.PixelFormat, ImageViewType.Type2D, ImageAspectFlags.ColorBit, 0, 1, 0, 1, default, ImageUsageFlags.TransferSrcBit);
return new ImageRequest(description, view, ImageRole.DisplaySurface);
}
}