sharpemu/src/SharpEmu.Libs/Gpu/Images/TileGeometry.cs

810 lines
32 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Numerics;
using SharpEmu.Libs.Gpu.Scheduling;
namespace SharpEmu.Libs.Gpu.Images;
public readonly record struct TileSizeAndAlignment(uint Size, uint Align);
// One mip level with 32-bit offsets and sizes: linear span, tiled span and tail location.
public struct TileLevelSpan
{
public uint Size;
public uint Offset;
public uint SourceSize;
public uint SourceOffset;
public uint X;
public uint Y;
}
public readonly record struct TilePaddedSize(uint Width, uint Height);
// The per-level output arrays of the layout queries, kept per thread: every draw that resolves a
// target or a texture needs them for a moment and reads them before the next query.
internal static class MipLayoutScratch
{
[ThreadStatic] private static TileLevelSpan[]? _spans;
[ThreadStatic] private static TilePaddedSize[]? _padded;
// Zeroed, as a new array would be, so a level the query leaves out reads as empty.
public static (TileLevelSpan[] Spans, TilePaddedSize[] Padded) Rent()
{
var spans = _spans ??= new TileLevelSpan[TiledSurfaceLayout.MaxLevels];
var padded = _padded ??= new TilePaddedSize[TiledSurfaceLayout.MaxLevels];
Array.Clear(spans);
Array.Clear(padded);
return (spans, padded);
}
}
public enum TileBlockKind : uint
{
Standard256B,
Standard4KB,
Standard4KB3D,
Standard64KB,
Standard64KB3D,
Prt64KB,
Prt64KB3D,
RenderTarget64KB,
Depth64KB,
RenderTarget64KBGen5,
}
public readonly record struct TileBlockLayout(TileBlockKind Kind, uint BytesPerElement, uint BlockSize, uint BlockWidth, uint BlockHeight, uint BlockDepth)
{
public bool IsValid => BytesPerElement != 0;
}
public readonly record struct TileElementLayout(uint Bytes, uint TexelWidth, uint TexelHeight);
public readonly record struct TileTextureBlockLayout(TileBlockLayout Block, uint TexelWidth, uint TexelHeight);
public enum TileSurfaceDimension : uint
{
Flat2D,
Volume3D,
}
public readonly record struct TiledSurfaceDescription(
GuestPixelFormat Format,
GuestTileMode TileMode,
TileSurfaceDimension Dimension,
uint Width,
uint Height,
uint Depth = 1,
uint Levels = 1,
uint Layers = 1);
// Width and height count addressable elements, not texels.
public struct TiledMipLayout
{
public ulong Offset;
public ulong Size;
public uint Width;
public uint Height;
public uint PaddedWidth;
public uint PaddedHeight;
public uint TailX;
public uint TailY;
}
public sealed class TiledSurfaceLayout
{
public const int MaxLevels = 16;
public TiledSurfaceDescription Description;
public TileTextureBlockLayout Texture;
public uint FirstTailLevel;
public ulong BlockSliceSize;
public ulong TotalSize;
public readonly TiledMipLayout[] Mips = new TiledMipLayout[MaxLevels];
}
// Block dimensions, tiled mip-chain placement and surface sizes of guest tile modes.
public static partial class TileGeometry
{
private readonly record struct Log2BlockDimensions(byte Width, byte Height, byte Depth);
// Indexed by log2 of the element size.
private static readonly Log2BlockDimensions[] Thin256B = [new(4, 4, 0), new(4, 3, 0), new(3, 3, 0), new(3, 2, 0), new(2, 2, 0)];
private static readonly Log2BlockDimensions[] Thin4KB = [new(6, 6, 0), new(6, 5, 0), new(5, 5, 0), new(5, 4, 0), new(4, 4, 0)];
private static readonly Log2BlockDimensions[] Thin64KB = [new(8, 8, 0), new(8, 7, 0), new(7, 7, 0), new(7, 6, 0), new(6, 6, 0)];
private static readonly Log2BlockDimensions[] Thick4KB = [new(4, 4, 4), new(3, 4, 4), new(3, 4, 3), new(3, 3, 3), new(2, 3, 3)];
private static readonly Log2BlockDimensions[] Thick64KB = [new(6, 5, 5), new(5, 5, 5), new(5, 5, 4), new(5, 4, 4), new(4, 4, 4)];
private readonly record struct BlockKindFacts(uint BlockSize, Log2BlockDimensions[] Dimensions, uint MaxBytesPerElement);
private static readonly BlockKindFacts[] BlockKinds =
[
new(256, Thin256B, 16),
new(4096, Thin4KB, 16),
new(4096, Thick4KB, 16),
new(65536, Thin64KB, 16),
new(65536, Thick64KB, 16),
new(65536, Thin64KB, 16),
new(65536, Thick64KB, 16),
new(65536, Thin64KB, 16),
new(65536, Thin64KB, 8),
new(65536, Thin64KB, 16),
];
private static readonly Log2BlockDimensions[][] MsaaBlocks =
[
[new(8, 8, 0), new(8, 7, 0), new(7, 7, 0), new(7, 6, 0), new(6, 6, 0)],
[new(7, 8, 0), new(7, 7, 0), new(6, 7, 0), new(6, 6, 0), new(5, 6, 0)],
[new(7, 7, 0), new(7, 6, 0), new(6, 6, 0), new(6, 5, 0), new(5, 5, 0)],
[new(6, 7, 0), new(6, 6, 0), new(5, 6, 0), new(5, 5, 0), new(4, 5, 0)],
];
private readonly record struct TailLocation(uint X, uint Y);
private static readonly TailLocation[][] TailThin4KB =
[
[new(32, 0), new(16, 32), new(0, 48), new(0, 32), new(16, 16), new(16, 0), new(0, 16), new(0, 0)],
[new(32, 0), new(16, 16), new(0, 24), new(0, 16), new(16, 8), new(16, 0), new(0, 8), new(0, 0)],
[new(16, 0), new(8, 16), new(0, 24), new(0, 16), new(8, 8), new(8, 0), new(0, 8), new(0, 0)],
[new(16, 0), new(8, 8), new(0, 12), new(0, 8), new(8, 4), new(8, 0), new(0, 4), new(0, 0)],
[new(8, 0), new(4, 8), new(0, 12), new(0, 8), new(4, 4), new(4, 0), new(0, 4), new(0, 0)],
];
private static readonly TailLocation[][] TailThin64KB =
[
[new(128, 0), new(0, 128), new(64, 0), new(0, 64), new(32, 0), new(16, 32), new(0, 48), new(0, 32), new(16, 16), new(16, 0), new(0, 16), new(0, 0)],
[new(128, 0), new(0, 64), new(64, 0), new(0, 32), new(32, 0), new(16, 16), new(0, 24), new(0, 16), new(16, 8), new(16, 0), new(0, 8), new(0, 0)],
[new(64, 0), new(0, 64), new(32, 0), new(0, 32), new(16, 0), new(8, 16), new(0, 24), new(0, 16), new(8, 8), new(8, 0), new(0, 8), new(0, 0)],
[new(64, 0), new(0, 32), new(32, 0), new(0, 16), new(16, 0), new(8, 8), new(0, 12), new(0, 8), new(8, 4), new(8, 0), new(0, 4), new(0, 0)],
[new(32, 0), new(0, 32), new(16, 0), new(0, 16), new(8, 0), new(4, 8), new(0, 12), new(0, 8), new(4, 4), new(4, 0), new(0, 4), new(0, 0)],
];
private static readonly TailLocation[][] TailThick64KB =
[
[new(32, 0), new(0, 16), new(16, 0), new(8, 8), new(0, 12), new(0, 8), new(8, 4), new(8, 0), new(0, 4), new(0, 0)],
[new(16, 0), new(0, 16), new(8, 0), new(4, 8), new(0, 12), new(0, 8), new(4, 4), new(4, 0), new(0, 4), new(0, 0)],
[new(16, 0), new(0, 16), new(8, 0), new(4, 8), new(0, 12), new(0, 8), new(4, 4), new(4, 0), new(0, 4), new(0, 0)],
[new(16, 0), new(0, 8), new(8, 0), new(4, 4), new(0, 6), new(0, 4), new(4, 2), new(4, 0), new(0, 2), new(0, 0)],
[new(8, 0), new(0, 8), new(4, 0), new(2, 4), new(0, 6), new(0, 4), new(2, 2), new(2, 0), new(0, 2), new(0, 0)],
];
private static readonly TailLocation[][] TailThick4KB =
[
[new(0, 8), new(8, 4), new(8, 0), new(0, 4), new(0, 0)],
[new(0, 8), new(4, 4), new(4, 0), new(0, 4), new(0, 0)],
[new(0, 8), new(4, 4), new(4, 0), new(0, 4), new(0, 0)],
[new(0, 4), new(4, 2), new(4, 0), new(0, 2), new(0, 0)],
[new(0, 4), new(2, 2), new(2, 0), new(0, 2), new(0, 0)],
];
private readonly record struct MipTailRule(TailLocation[] Locations, uint WidthLimit, uint HeightLimit)
{
public uint MaxLevels => (uint)Locations.Length;
}
private static uint AlignUp(uint value, uint alignment) => (value + alignment - 1) & ~(alignment - 1);
private static uint ShiftCeil(uint value, uint shift) => (uint)(((ulong)value + (1UL << (int)shift) - 1) >> (int)shift);
private static uint LinearBlockWidth(uint bytesPerElement) => 256 / bytesPerElement;
private static int Log2(uint value) => BitOperations.TrailingZeroCount(value);
private static uint LinearAlignedLevelPitch(uint baseWidth, uint baseHeight, uint level, uint bytesPerElement, out uint paddedHeight, out uint levelSize)
{
var levelWidth = ShiftCeil(baseWidth, level);
var levelHeight = ShiftCeil(baseHeight, level);
var paddedWidth = AlignUp(Math.Max(levelWidth, 1), LinearBlockWidth(bytesPerElement));
var size = (ulong)paddedWidth * Math.Max(levelHeight, 1) * bytesPerElement;
if (size > uint.MaxValue)
{
throw SubmissionScheduler.Fatal($"A linear mip level is too large: level={level} size={size}.");
}
paddedHeight = Math.Max(levelHeight, 1);
levelSize = (uint)size;
return paddedWidth;
}
// Smaller mip records come first; mip 0 is last in the block slice.
private static uint SetLinearMipChainLayout(uint levels, uint[] mipPitch, uint[] mipHeight, uint[] mipSize, TileLevelSpan[]? levelSpans, TilePaddedSize[]? paddedSizes)
{
uint offset = 0;
for (var level = (int)levels - 1; level >= 0; level--)
{
if (levelSpans != null)
{
levelSpans[level].Size = mipSize[level];
levelSpans[level].Offset = offset;
}
if (paddedSizes != null)
{
paddedSizes[level] = new TilePaddedSize(mipPitch[level], mipHeight[level]);
}
offset += mipSize[level];
}
return AlignUp(offset, 256);
}
private static (uint Width, uint Height, uint Depth) BlockDimensions(Log2BlockDimensions dimensions) =>
(1u << dimensions.Width, 1u << dimensions.Height, 1u << dimensions.Depth);
private static bool TryGetMsaa64KBBlock(uint bytesPerElement, uint fragmentsLog2, out uint blockWidth, out uint blockHeight)
{
blockWidth = blockHeight = 0;
if (fragmentsLog2 > 3 || !BitOperations.IsPow2(bytesPerElement) || bytesPerElement > 16)
{
return false;
}
(blockWidth, blockHeight, _) = BlockDimensions(MsaaBlocks[fragmentsLog2][Log2(bytesPerElement)]);
return true;
}
private static bool TryGetMipTailRule(TileBlockLayout block, out MipTailRule rule)
{
var index = Log2(block.BytesPerElement);
switch (block.Kind)
{
case TileBlockKind.Standard4KB:
rule = new MipTailRule(TailThin4KB[index], block.BlockWidth >> 1, block.BlockHeight);
return true;
case TileBlockKind.Standard4KB3D:
rule = new MipTailRule(TailThick4KB[index], block.BlockWidth, block.BlockHeight >> 1);
return true;
case TileBlockKind.Standard64KB3D:
case TileBlockKind.Prt64KB3D:
rule = new MipTailRule(TailThick64KB[index], block.BlockWidth >> 1, block.BlockHeight);
return true;
case TileBlockKind.Depth64KB:
rule = block.BytesPerElement < 4
? new MipTailRule(TailThin64KB[index], 64, 128)
: new MipTailRule(TailThin64KB[index], block.BlockWidth >> 1, block.BlockHeight);
return true;
case TileBlockKind.Standard64KB:
case TileBlockKind.Prt64KB:
case TileBlockKind.RenderTarget64KB:
case TileBlockKind.RenderTarget64KBGen5:
rule = new MipTailRule(TailThin64KB[index], block.BlockWidth >> 1, block.BlockHeight);
return true;
default:
rule = default;
return false;
}
}
public static bool TryGetElementLayout(GuestPixelFormat format, out TileElementLayout layout)
{
var bytes = GuestPixelFormats.BytesPerElement(format);
if (BitOperations.IsPow2(bytes))
{
layout = new TileElementLayout(bytes, 1, 1);
return true;
}
var blockBytes = GuestPixelFormats.BlockCompressedBytes(format);
if (blockBytes != 0)
{
layout = new TileElementLayout(blockBytes, 4, 4);
return true;
}
layout = default;
return false;
}
public static bool TryGetTextureBlockLayout(GuestPixelFormat format, GuestTileMode tile, bool volume, out TileTextureBlockLayout layout)
{
layout = default;
TileBlockKind kind;
switch (tile)
{
case GuestTileMode.Standard256B when !volume:
kind = TileBlockKind.Standard256B;
break;
case GuestTileMode.Standard4KB:
kind = volume ? TileBlockKind.Standard4KB3D : TileBlockKind.Standard4KB;
break;
case GuestTileMode.Standard64KB:
kind = volume ? TileBlockKind.Standard64KB3D : TileBlockKind.Standard64KB;
break;
case GuestTileMode.Prt:
kind = volume ? TileBlockKind.Prt64KB3D : TileBlockKind.Prt64KB;
break;
case GuestTileMode.Depth:
kind = TileBlockKind.Depth64KB;
break;
case GuestTileMode.RenderTarget:
kind = TileBlockKind.RenderTarget64KBGen5;
break;
default:
return false;
}
if (!TryGetElementLayout(format, out var element))
{
return false;
}
if (kind is TileBlockKind.Depth64KB or TileBlockKind.RenderTarget64KB or TileBlockKind.RenderTarget64KBGen5 &&
GuestPixelFormats.RenderTargetBytesPerElement(format) != element.Bytes)
{
return false;
}
if (!TryGetBlockLayout(kind, element.Bytes, out var block))
{
return false;
}
layout = new TileTextureBlockLayout(block, element.TexelWidth, element.TexelHeight);
return true;
}
public static bool TryGetTiledTextureLayout(in TiledSurfaceDescription description, out TiledSurfaceLayout layout)
{
layout = null!;
var volume = description.Dimension == TileSurfaceDimension.Volume3D;
if (description.Dimension > TileSurfaceDimension.Volume3D ||
description.Width == 0 || description.Height == 0 || description.Depth == 0 ||
description.Levels == 0 || description.Levels > TiledSurfaceLayout.MaxLevels || description.Layers == 0 ||
(volume ? description.Layers != 1 : description.Depth != 1))
{
return false;
}
if (!TryGetTextureBlockLayout(description.Format, description.TileMode, volume, out var texture))
{
return false;
}
var block = texture.Block;
var width0 = (description.Width + texture.TexelWidth - 1) / texture.TexelWidth;
var height0 = (description.Height + texture.TexelHeight - 1) / texture.TexelHeight;
var result = new TiledSurfaceLayout
{
Description = description,
Texture = texture,
FirstTailLevel = description.Levels,
};
var blockSlices = volume ? ShiftCeil(description.Depth, (uint)Log2(block.BlockDepth)) : description.Layers;
var hasTail = TryGetMipTailRule(block, out var tail);
if (hasTail && description.Levels > 1)
{
for (uint level = 0; level < description.Levels; level++)
{
if (ShiftCeil(width0, level) <= tail.WidthLimit &&
ShiftCeil(height0, level) <= tail.HeightLimit &&
description.Levels - level <= tail.MaxLevels)
{
result.FirstTailLevel = level;
break;
}
}
}
for (uint level = 0; level < result.FirstTailLevel; level++)
{
ref var mip = ref result.Mips[level];
mip.Width = Math.Max(((description.Width >> (int)level) + texture.TexelWidth - 1) / texture.TexelWidth, 1);
mip.Height = Math.Max(((description.Height >> (int)level) + texture.TexelHeight - 1) / texture.TexelHeight, 1);
mip.PaddedWidth = AlignUp(Math.Max(ShiftCeil(width0, level), 1), block.BlockWidth);
mip.PaddedHeight = AlignUp(Math.Max(ShiftCeil(height0, level), 1), block.BlockHeight);
mip.Size = (ulong)block.BlockDepth * mip.PaddedWidth * mip.PaddedHeight * block.BytesPerElement;
result.BlockSliceSize += mip.Size;
}
if (result.FirstTailLevel < description.Levels)
{
result.BlockSliceSize += block.BlockSize;
}
for (var level = result.FirstTailLevel; level < description.Levels; level++)
{
ref var mip = ref result.Mips[level];
mip.Width = Math.Max(((description.Width >> (int)level) + texture.TexelWidth - 1) / texture.TexelWidth, 1);
mip.Height = Math.Max(((description.Height >> (int)level) + texture.TexelHeight - 1) / texture.TexelHeight, 1);
mip.PaddedWidth = block.BlockWidth;
mip.PaddedHeight = block.BlockHeight;
mip.Size = block.BlockSize;
var location = tail.Locations[level - result.FirstTailLevel];
mip.TailX = location.X;
mip.TailY = location.Y;
}
var offset = result.FirstTailLevel < description.Levels ? block.BlockSize : 0UL;
for (var level = (int)result.FirstTailLevel - 1; level >= 0; level--)
{
result.Mips[level].Offset = offset;
offset += result.Mips[level].Size;
}
if (offset != result.BlockSliceSize || result.BlockSliceSize > ulong.MaxValue / blockSlices)
{
return false;
}
result.TotalSize = result.BlockSliceSize * blockSlices;
layout = result;
return true;
}
private static void Set32BitTiledMipLayout(TiledSurfaceLayout layout, out TileSizeAndAlignment totalSize, TileLevelSpan[]? levelSpans, TilePaddedSize[]? paddedSizes)
{
if (layout.BlockSliceSize > uint.MaxValue)
{
throw SubmissionScheduler.Fatal($"A tiled block slice exceeds the 32-bit size limit: size={layout.BlockSliceSize}.");
}
var block = layout.Texture.Block;
totalSize = new TileSizeAndAlignment((uint)layout.BlockSliceSize, block.BlockSize);
ulong linearTailOffset = 0;
for (uint level = 0; level < layout.Description.Levels; level++)
{
ref var mip = ref layout.Mips[level];
var tail = level >= layout.FirstTailLevel;
if (mip.Offset > uint.MaxValue || mip.Size > uint.MaxValue)
{
throw SubmissionScheduler.Fatal($"A tiled mip level exceeds the 32-bit offset or size limit: level={level} offset={mip.Offset} size={mip.Size}.");
}
if (levelSpans != null)
{
if (tail)
{
var linearSize = (ulong)mip.Width * mip.Height * block.BytesPerElement;
if (linearSize > uint.MaxValue || linearTailOffset > uint.MaxValue)
{
throw SubmissionScheduler.Fatal($"A mip-tail level exceeds the 32-bit offset or size limit: level={level} size={linearSize} offset={linearTailOffset}.");
}
levelSpans[level] = new TileLevelSpan
{
Size = (uint)linearSize,
Offset = (uint)linearTailOffset,
SourceSize = (uint)mip.Size,
SourceOffset = (uint)mip.Offset,
X = mip.TailX,
Y = mip.TailY,
};
linearTailOffset += linearSize;
}
else if (block.Kind == TileBlockKind.Standard256B)
{
levelSpans[level] = new TileLevelSpan { Size = (uint)mip.Size, Offset = (uint)mip.Offset };
}
else
{
levelSpans[level] = new TileLevelSpan
{
Size = (uint)mip.Size,
Offset = (uint)mip.Offset,
SourceSize = (uint)mip.Size,
SourceOffset = (uint)mip.Offset,
};
}
}
if (paddedSizes != null)
{
paddedSizes[level] = new TilePaddedSize(mip.PaddedWidth * layout.Texture.TexelWidth, mip.PaddedHeight * layout.Texture.TexelHeight);
}
}
}
public static bool TryGetBlockLayout(TileBlockKind kind, uint bytesPerElement, out TileBlockLayout layout)
{
layout = default;
if ((uint)kind >= BlockKinds.Length || !BitOperations.IsPow2(bytesPerElement))
{
return false;
}
var facts = BlockKinds[(int)kind];
if (bytesPerElement > facts.MaxBytesPerElement)
{
return false;
}
var (width, height, depth) = BlockDimensions(facts.Dimensions[Log2(bytesPerElement)]);
if ((ulong)width * height * depth * bytesPerElement != facts.BlockSize)
{
return false;
}
layout = new TileBlockLayout(kind, bytesPerElement, facts.BlockSize, width, height, depth);
return true;
}
private static bool TryGetHtileSize(uint width, uint height, out TileSizeAndAlignment htileSize)
{
htileSize = default;
if (width == 0 || width > 16384 || height == 0 || height > 16384)
{
return false;
}
// HTile keeps one dword per depth tile; 32 KiB blocks cover 1024x512 pixels.
var size = (ulong)(AlignUp(width, 1024) / 1024) * (AlignUp(height, 512) / 512) * 32768;
if (size == 0 || size > uint.MaxValue)
{
return false;
}
htileSize = new TileSizeAndAlignment((uint)size, 32768);
return true;
}
// Uncompressed depth and stencil are independent 64 KiB block surfaces.
public static bool TryGetDepthSize(
uint width,
uint height,
uint pitch,
GuestDepthFormat depthFormat,
GuestStencilFormat stencilFormat,
bool htile,
out TileSizeAndAlignment stencilSize,
out TileSizeAndAlignment htileSize,
out TileSizeAndAlignment depthSize,
uint fragmentsLog2 = 0)
{
if (pitch != 0)
{
throw SubmissionScheduler.Fatal($"A depth surface size query must not carry a pitch: pitch={pitch}.");
}
depthSize = htileSize = stencilSize = default;
if (width == 0 || width > 16384 || height == 0 || height > 16384 ||
depthFormat is not (GuestDepthFormat.Z16 or GuestDepthFormat.Z32Float) ||
stencilFormat is not (GuestStencilFormat.Invalid or GuestStencilFormat.Stencil8UInt) ||
fragmentsLog2 > 3)
{
return false;
}
var depthBytes = depthFormat == GuestDepthFormat.Z16 ? 2u : 4u;
uint stencilBlockWidth = 0, stencilBlockHeight = 0;
var validBlocks = TryGetMsaa64KBBlock(depthBytes, fragmentsLog2, out var depthBlockWidth, out var depthBlockHeight) &&
(stencilFormat == GuestStencilFormat.Invalid || TryGetMsaa64KBBlock(1, fragmentsLog2, out stencilBlockWidth, out stencilBlockHeight));
var fragments = 1u << (int)fragmentsLog2;
var depthBytesTotal = validBlocks
? (ulong)AlignUp(width, depthBlockWidth) * AlignUp(height, depthBlockHeight) * depthBytes * fragments
: 0;
var stencilBytesTotal = stencilFormat == GuestStencilFormat.Stencil8UInt && validBlocks
? (ulong)AlignUp(width, stencilBlockWidth) * AlignUp(height, stencilBlockHeight) * fragments
: 0;
var htileValid = !htile || TryGetHtileSize(width, height, out htileSize);
if (depthBytesTotal > uint.MaxValue || stencilBytesTotal > uint.MaxValue || !htileValid)
{
depthSize = htileSize = stencilSize = default;
return false;
}
depthSize = new TileSizeAndAlignment((uint)depthBytesTotal, 65536);
stencilSize = stencilFormat == GuestStencilFormat.Stencil8UInt ? new TileSizeAndAlignment((uint)stencilBytesTotal, 65536) : default;
return true;
}
public static uint RenderTargetPitch(uint width, uint bytesPerElement, uint fragmentsLog2 = 0)
{
if (width == 0 || !TryGetMsaa64KBBlock(bytesPerElement, fragmentsLog2, out var blockWidth, out _))
{
return 0;
}
var pitch = ((ulong)width + blockWidth - 1) & ~(ulong)(blockWidth - 1);
return pitch <= uint.MaxValue ? (uint)pitch : 0;
}
public static uint DepthPitch(uint width, uint bytesPerElement, uint fragmentsLog2 = 0) =>
RenderTargetPitch(width, bytesPerElement, fragmentsLog2);
public static bool TryGetRenderTargetSize(uint width, uint height, uint pitch, uint bytesPerElement, out TileSizeAndAlignment totalSize, uint fragmentsLog2 = 0)
{
totalSize = default;
if (height == 0 || pitch == 0 ||
!TryGetMsaa64KBBlock(bytesPerElement, fragmentsLog2, out _, out var blockHeight) ||
pitch != RenderTargetPitch(width, bytesPerElement, fragmentsLog2))
{
return false;
}
var paddedHeight = ((ulong)height + blockHeight - 1) & ~(ulong)(blockHeight - 1);
var size = pitch * paddedHeight * bytesPerElement * (1u << (int)fragmentsLog2);
if (size == 0 || size > uint.MaxValue)
{
return false;
}
totalSize = new TileSizeAndAlignment((uint)size, 65536);
return true;
}
public static bool TryGetDccSize(
uint width,
uint height,
uint slices,
uint bytesPerElement,
uint levels,
GuestTileMode tile,
out TileSizeAndAlignment totalSize,
uint fragmentsLog2 = 0)
{
totalSize = default;
if (width == 0 || height == 0 || slices == 0 || levels != 1 || fragmentsLog2 != 0 ||
!BitOperations.IsPow2(bytesPerElement) || bytesPerElement > 16 ||
tile is not (GuestTileMode.RenderTarget or GuestTileMode.Depth))
{
return false;
}
var coverageBits = 20 - BitOperations.TrailingZeroCount(bytesPerElement);
var blockWidth = 1u << ((coverageBits + 1) / 2);
var blockHeight = 1u << (coverageBits / 2);
var blocksX = ((ulong)width + blockWidth - 1) / blockWidth;
var blocksY = ((ulong)height + blockHeight - 1) / blockHeight;
var blocks = blocksX * blocksY;
if (blocks == 0 || blocks > uint.MaxValue / 4096u / slices)
{
return false;
}
totalSize = new TileSizeAndAlignment((uint)(blocks * slices * 4096u), 4096);
return true;
}
public static bool TryGetRenderTargetMipLayout(uint width, uint height, uint pitch, uint bytesPerElement, uint levels, out TileSizeAndAlignment totalSize, TileLevelSpan[]? levelSpans, TilePaddedSize[]? paddedSizes)
{
totalSize = default;
if (width == 0 || height == 0 || levels == 0 || levels > TiledSurfaceLayout.MaxLevels || pitch != RenderTargetPitch(width, bytesPerElement))
{
return false;
}
uint maxLevels = 1;
var maxDimension = Math.Max(width, height);
while (maxDimension > 1)
{
maxDimension >>= 1;
maxLevels++;
}
if (levels > maxLevels)
{
return false;
}
var format = bytesPerElement switch
{
1 => GuestPixelFormat.Bits8UNorm,
2 => GuestPixelFormat.Bits16UNorm,
4 => GuestPixelFormat.Bits32Float,
8 => GuestPixelFormat.Bits16_16_16_16Float,
16 => GuestPixelFormat.Bits32_32_32_32Float,
_ => GuestPixelFormat.Invalid,
};
if (format == GuestPixelFormat.Invalid)
{
return false;
}
return TryGetTextureSize(format, width, height, levels, GuestTileMode.RenderTarget, out totalSize, levelSpans, paddedSizes) &&
totalSize.Size != 0 && totalSize.Align == 65536;
}
// False when a tiled layout cannot be computed; linear layouts always succeed.
public static bool TryGetTextureSize(GuestPixelFormat format, uint width, uint height, uint levels, GuestTileMode tile, out TileSizeAndAlignment totalSize, TileLevelSpan[]? levelSpans, TilePaddedSize[]? paddedSizes)
{
if (levels == 0 || levels > TiledSurfaceLayout.MaxLevels)
{
throw SubmissionScheduler.Fatal($"The mip level count is out of range: levels={levels}.");
}
totalSize = default;
if (tile == GuestTileMode.Linear && TryGetElementLayout(format, out var element))
{
var mipPitch = new uint[TiledSurfaceLayout.MaxLevels];
var mipHeight = new uint[TiledSurfaceLayout.MaxLevels];
var mipSize = new uint[TiledSurfaceLayout.MaxLevels];
var elementsWidth0 = Math.Max((width + element.TexelWidth - 1) / element.TexelWidth, 1);
var elementsHeight0 = Math.Max((height + element.TexelHeight - 1) / element.TexelHeight, 1);
var compressed = element.TexelWidth != 1 || element.TexelHeight != 1;
for (uint level = 0; level < levels; level++)
{
var alignedElementsWidth = LinearAlignedLevelPitch(elementsWidth0, elementsHeight0, level, element.Bytes, out var paddedElementsHeight, out mipSize[level]);
mipPitch[level] = alignedElementsWidth * element.TexelWidth;
mipHeight[level] = paddedElementsHeight * element.TexelHeight;
if (compressed)
{
mipPitch[level] = Math.Max(mipPitch[level], 32);
mipHeight[level] = Math.Max(mipHeight[level], 32);
}
}
var total = SetLinearMipChainLayout(levels, mipPitch, mipHeight, mipSize, levelSpans, paddedSizes);
totalSize = new TileSizeAndAlignment(total, 256);
return true;
}
var description = new TiledSurfaceDescription(format, tile, TileSurfaceDimension.Flat2D, width, height, 1, levels, 1);
if (!TryGetTiledTextureLayout(description, out var layout))
{
return false;
}
Set32BitTiledMipLayout(layout, out totalSize, levelSpans, paddedSizes);
return true;
}
public static TileSizeAndAlignment TextureTotalSize(GuestPixelFormat format, uint width, uint height, uint depth, uint levels, GuestTileMode tile, bool volume)
{
if (depth == 0)
{
throw SubmissionScheduler.Fatal("A texture size query needs a depth of at least one.");
}
if (volume && tile != GuestTileMode.Linear)
{
var description = new TiledSurfaceDescription(format, tile, TileSurfaceDimension.Volume3D, width, height, depth, levels, 1);
if (!TryGetTiledTextureLayout(description, out var layout))
{
throw SubmissionScheduler.Fatal($"The 3D texture layout is not supported: format={(uint)format} tile={(uint)tile} extent={width}x{height}x{depth} levels={levels}.");
}
if (layout.TotalSize > uint.MaxValue)
{
throw SubmissionScheduler.Fatal($"The 3D texture is too large: size={layout.TotalSize}.");
}
return new TileSizeAndAlignment((uint)layout.TotalSize, layout.Texture.Block.BlockSize);
}
if (!TryGetTextureSize(format, width, height, levels, tile, out var sliceSize, null, null))
{
throw SubmissionScheduler.Fatal($"The texture layout is not supported: format={(uint)format} width={width} height={height} levels={levels} tile={(uint)tile}.");
}
var total = (ulong)sliceSize.Size * depth;
if (total > uint.MaxValue)
{
throw SubmissionScheduler.Fatal($"The layered texture is too large: size={total}.");
}
return new TileSizeAndAlignment((uint)total, sliceSize.Align);
}
public static uint TexturePitch(GuestPixelFormat format, uint width, GuestTileMode tile)
{
var pitch = width;
switch (tile)
{
case GuestTileMode.Linear:
if (TryGetElementLayout(format, out var element) && element.TexelWidth == 1 && element.TexelHeight == 1)
{
pitch = AlignUp(pitch, LinearBlockWidth(element.Bytes));
}
break;
case GuestTileMode.Standard4KB:
case GuestTileMode.Standard64KB:
case GuestTileMode.Prt:
case GuestTileMode.Depth:
case GuestTileMode.RenderTarget:
if (TryGetTextureBlockLayout(format, tile, false, out var layout))
{
pitch = AlignUp(pitch, layout.Block.BlockWidth * layout.TexelWidth);
}
break;
}
return pitch;
}
}