sharpemu/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.DynamicState.cs

260 lines
9.9 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Libs.Gpu.GpuCommands.Registers;
using SharpEmu.Libs.VideoOut;
using Silk.NET.Vulkan;
namespace SharpEmu.Libs.Gpu.Rendering;
public sealed partial class RenderExecutor
{
private const uint MaxViewportDimension = 16384;
// A guest bias for a fixed-point depth format scales by the host depth bits.
public static float DepthBiasConstantFactor(float guestFactor, in PolygonOffsetRegisters offset, Format hostDepthFormat)
{
if (offset.DepthIsFloat)
{
return guestFactor;
}
var hostDepthBits = hostDepthFormat switch
{
Format.D16Unorm or Format.D16UnormS8Uint => 16,
Format.D24UnormS8Uint => 24,
_ => 0,
};
return hostDepthBits == 0 ? guestFactor : MathF.ScaleB(guestFactor, hostDepthBits + offset.NegativeDepthBits);
}
// The dynamic state of a draw from the context bank and the resolved targets.
private DynamicDrawState BuildDynamicState(ContextRegisters context, in DrawState state)
{
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.DrawDynamicStatePreparation);
var viewportRegisters = context.ScreenViewport;
var limits = _host.Limits;
uint framebufferWidth;
uint framebufferHeight;
if (state.ColorCount > 0 && state.Colors[0].Image.IsValid)
{
framebufferWidth = state.Colors[0].Resolution.Extent.Width;
framebufferHeight = state.Colors[0].Resolution.Extent.Height;
}
else if (state.Depth.Image.IsValid)
{
framebufferWidth = state.Depth.Target.Target.Width;
framebufferHeight = state.Depth.Target.Target.Height;
}
else
{
framebufferWidth = limits.MaxFramebufferWidth;
framebufferHeight = limits.MaxFramebufferHeight;
}
var scissor = ResolveScissor(viewportRegisters, context.ScanMode, framebufferWidth, framebufferHeight);
ref readonly var viewport = ref viewportRegisters.Viewports[0];
float viewportX;
float viewportY;
float viewportWidth;
float viewportHeight;
if (context.Clip.ClipDisable)
{
viewportX = 0;
viewportY = 0;
viewportWidth = Math.Min(limits.MaxViewportWidth, MaxViewportDimension);
viewportHeight = Math.Min(limits.MaxViewportHeight, MaxViewportDimension);
}
else
{
viewportX = viewport.XOffset - viewport.XScale;
viewportY = viewport.YOffset - viewport.YScale;
viewportWidth = viewport.XScale * 2f;
viewportHeight = viewport.YScale * 2f;
}
var lineWidth = context.LineWidth;
if (lineWidth != 1f)
{
if (RenderTrace.Enabled && RenderTrace.LineWidthClamp())
{
RenderTrace.Write($"Clamping the line width to 1.0 because wide lines are not enabled: lineWidth={lineWidth}");
}
lineWidth = 1f;
}
var depthTarget = state.Depth.HasTarget ? state.Depth.Target : default;
var depthState = depthTarget.State;
var depthFormat = depthTarget.Target.Format;
var mode = context.RasterMode;
var polygonOffset = context.PolygonOffset;
var useFront = mode.PolygonOffsetFrontEnable && !mode.CullFront;
var useBack = mode.PolygonOffsetBackEnable && !mode.CullBack;
var depthBiasEnabled = useFront || useBack;
var depthBiasConstant = 0f;
var depthBiasSlope = 0f;
if (depthBiasEnabled)
{
// One host bias serves both faces; a visible front face wins.
var guestConstant = useFront ? polygonOffset.FrontOffset : polygonOffset.BackOffset;
depthBiasConstant = DepthBiasConstantFactor(guestConstant, in polygonOffset, depthFormat);
depthBiasSlope = (useFront ? polygonOffset.FrontScale : polygonOffset.BackScale) / 16f;
}
byte colorWriteMask = 0;
for (var i = 0; i < state.ColorCount; i++)
{
if (context.RenderTargetMaskForSlot(state.Colors[i].Slot) != 0)
{
colorWriteMask |= (byte)(1 << i);
}
}
var blend = context.BlendColor;
return new DynamicDrawState(
viewportX,
viewportY,
viewportWidth,
viewportHeight,
viewport.ZOffset - (context.Clip.DirectXClipSpace ? 0f : viewport.ZScale),
viewport.ZScale + viewport.ZOffset,
scissor,
lineWidth,
blend.Red,
blend.Green,
blend.Blue,
blend.Alpha,
depthState.DepthTestEnabled,
depthState.DepthWriteEnabled && !depthState.DepthClearEnabled,
depthState.DepthCompare,
depthBiasEnabled,
depthBiasConstant,
depthBiasEnabled ? polygonOffset.Clamp : 0f,
depthBiasSlope,
depthState.StencilTestEnabled,
depthState.FrontMasks,
depthState.BackMasks,
state.ColorCount,
colorWriteMask);
}
// The screen, window, generic, viewport and clip rectangles intersected and clamped to the extent.
public static ScissorRectangle ResolveScissor(ScreenViewportRegisters viewport, in ScanModeRegisters scanMode, uint width, uint height)
{
var screen = new ScissorRectangle(viewport.ScreenScissorLeft, viewport.ScreenScissorTop, viewport.ScreenScissorRight, viewport.ScreenScissorBottom);
var final = screen;
if (!screen.IsSet)
{
final = new ScissorRectangle(0, 0, (int)width, (int)height);
if (RenderTrace.Enabled && RenderTrace.ScissorDefault())
{
RenderTrace.Write($"Defaulting an unset screen scissor to the framebuffer extent: width={width} height={height}");
}
}
var window = new ScissorRectangle(viewport.WindowScissorLeft, viewport.WindowScissorTop, viewport.WindowScissorRight, viewport.WindowScissorBottom);
if (window.IsSet)
{
if (viewport.WindowScissorWindowOffsetEnable)
{
window = window.Offset(viewport.WindowOffsetX, viewport.WindowOffsetY);
}
final = final.Intersect(in window);
}
var generic = new ScissorRectangle(viewport.GenericScissorLeft, viewport.GenericScissorTop, viewport.GenericScissorRight, viewport.GenericScissorBottom);
if (generic.IsSet)
{
if (viewport.GenericScissorWindowOffsetEnable)
{
generic = generic.Offset(viewport.WindowOffsetX, viewport.WindowOffsetY);
}
final = final.Intersect(in generic);
}
ref readonly var first = ref viewport.Viewports[0];
var viewportScissor = new ScissorRectangle(first.ScissorLeft, first.ScissorTop, first.ScissorRight, first.ScissorBottom);
if (scanMode.ViewportScissorEnable && viewportScissor.IsSet)
{
if (first.ScissorWindowOffsetEnable)
{
viewportScissor = viewportScissor.Offset(viewport.WindowOffsetX, viewport.WindowOffsetY);
}
final = final.Intersect(in viewportScissor);
}
if (viewport.ClipRectangleRule == 0)
{
final = new ScissorRectangle(0, 0, 0, 0);
}
else if (viewport.ClipRectangleRule != 0xFFFF)
{
if (TryClipRuleToIntersectionMask(viewport.ClipRectangleRule, out var clipMask))
{
for (var i = 0; i < ScreenViewportRegisters.ClipRectangleCount; i++)
{
if ((clipMask & (1 << i)) == 0)
{
continue;
}
var clip = new ScissorRectangle(viewport.ClipRectangleLeft[i], viewport.ClipRectangleTop[i], viewport.ClipRectangleRight[i], viewport.ClipRectangleBottom[i]);
if (viewport.ClipRectangleWindowOffsetEnable[i])
{
clip = clip.Offset(viewport.WindowOffsetX, viewport.WindowOffsetY);
}
final = final.Intersect(in clip);
}
}
else if (RenderTrace.Enabled && RenderTrace.ClipRuleWarning())
{
RenderTrace.Write($"Leaving the scissor unchanged for an unsupported clip rectangle rule: rule=0x{viewport.ClipRectangleRule:X4}");
}
}
return ClampScissor(final, width, height);
}
private static ScissorRectangle ClampScissor(ScissorRectangle rectangle, uint width, uint height)
{
var maxRight = (int)width;
var maxBottom = (int)height;
var clamped = new ScissorRectangle(
Math.Clamp(rectangle.Left, 0, maxRight),
Math.Clamp(rectangle.Top, 0, maxBottom),
Math.Clamp(rectangle.Right, 0, maxRight),
Math.Clamp(rectangle.Bottom, 0, maxBottom));
return clamped.IsValid ? clamped : clamped with { Right = clamped.Left, Bottom = clamped.Top };
}
// A rule is the set of rectangle combinations that pass; only "all of a subset" rules are supported.
private static bool TryClipRuleToIntersectionMask(ushort rule, out byte mask)
{
for (var candidate = 0u; candidate < 16; candidate++)
{
ushort combinations = 0;
for (var combination = 0u; combination < 16; combination++)
{
if ((combination & candidate) == candidate)
{
combinations |= (ushort)(1u << (int)combination);
}
}
if (combinations == rule)
{
mask = (byte)candidate;
return true;
}
}
mask = 0;
return false;
}
}