Fix: Metal scissor calculation (#5896)
This commit is contained in:
@@ -851,17 +851,11 @@ void MetalDriver::beginRenderPass(Handle<HwRenderTarget> rth,
|
||||
encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
MTLRegion mvp = renderTarget->getRegionFromClientRect(params.viewport);
|
||||
MTLViewport metalViewport {
|
||||
.originX = static_cast<double>(mvp.origin.x),
|
||||
.originY = static_cast<double>(mvp.origin.y),
|
||||
.width = static_cast<double>(mvp.size.width),
|
||||
.height = static_cast<double>(mvp.size.height),
|
||||
.znear = static_cast<double>(params.depthRange.near),
|
||||
.zfar = static_cast<double>(params.depthRange.far)
|
||||
};
|
||||
MTLViewport metalViewport =
|
||||
renderTarget->getViewportFromClientViewport(params.viewport,
|
||||
params.depthRange.near,
|
||||
params.depthRange.far);
|
||||
[mContext->currentRenderPassEncoder setViewport:metalViewport];
|
||||
|
||||
mContext->currentViewport = metalViewport;
|
||||
|
||||
// Metal requires a new command encoder for each render pass, and they cannot be reused.
|
||||
@@ -1287,20 +1281,37 @@ void MetalDriver::draw(PipelineState ps, Handle<HwRenderPrimitive> rph, uint32_t
|
||||
}
|
||||
|
||||
// Set scissor-rectangle.
|
||||
// In order to do this, we compute the intersection between:
|
||||
// 1. the scissor rectangle
|
||||
// 2. the current viewport
|
||||
// 3. the render target attachment dimensions (important, as the scissor can't be set larger)
|
||||
// fmax/min are used below to guard against NaN and because the MTLViewport/MTLRegion
|
||||
// coordinates are doubles.
|
||||
MTLRegion scissor = mContext->currentRenderTarget->getRegionFromClientRect(ps.scissor);
|
||||
const MTLViewport& viewport = mContext->currentViewport;
|
||||
const float sleft = scissor.origin.x, sright = scissor.origin.x + scissor.size.width;
|
||||
const float stop = scissor.origin.y, sbottom = scissor.origin.y + scissor.size.height;
|
||||
|
||||
// fmax/min are used here to guard against NaN and because the MTLViewport coordinates are doubles.
|
||||
const auto left = std::fmax(viewport.originX , scissor.origin.x );
|
||||
const auto right = std::fmin(viewport.originX + viewport.width , scissor.origin.x + scissor.size.width );
|
||||
const auto top = std::fmax(viewport.originY , scissor.origin.y );
|
||||
const auto bottom = std::fmin(viewport.originY + viewport.height, scissor.origin.y + scissor.size.height );
|
||||
// Viewport extent
|
||||
const MTLViewport& viewport = mContext->currentViewport;
|
||||
const float vleft = viewport.originX, vright = viewport.originX + viewport.width;
|
||||
const float vtop = viewport.originY, vbottom = viewport.originY + viewport.height;
|
||||
|
||||
// Attachment extent
|
||||
const auto attachmentSize = mContext->currentRenderTarget->getAttachmentSize();
|
||||
const float aleft = 0.0f, atop = 0.0f;
|
||||
const float aright = static_cast<float>(attachmentSize.x);
|
||||
const float abottom = static_cast<float>(attachmentSize.y);
|
||||
|
||||
const auto left = std::fmax(std::fmax(sleft, vleft), aleft);
|
||||
const auto right = std::fmin(std::fmin(sright, vright), aright);
|
||||
const auto top = std::fmax(std::fmax(stop, vtop), atop);
|
||||
const auto bottom = std::fmin(std::fmin(sbottom, vbottom), abottom);
|
||||
|
||||
MTLScissorRect scissorRect = {
|
||||
.x = static_cast<NSUInteger>(left),
|
||||
.y = static_cast<NSUInteger>(top ),
|
||||
.y = static_cast<NSUInteger>(top),
|
||||
.width = static_cast<NSUInteger>(right - left),
|
||||
.height = static_cast<NSUInteger>(bottom - top )
|
||||
.height = static_cast<NSUInteger>(bottom - top)
|
||||
};
|
||||
|
||||
[mContext->currentRenderPassEncoder setScissorRect:scissorRect];
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
#include <utils/FixedCapacityVector.h>
|
||||
#include <utils/Panic.h>
|
||||
|
||||
#include <math/vec2.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <memory>
|
||||
@@ -288,8 +290,21 @@ public:
|
||||
|
||||
void setUpRenderPassAttachments(MTLRenderPassDescriptor* descriptor, const RenderPassParams& params);
|
||||
|
||||
MTLViewport getViewportFromClientViewport(
|
||||
Viewport rect, float depthRangeNear, float depthRangeFar) {
|
||||
const int32_t height = int32_t(getAttachmentSize().y);
|
||||
assert_invariant(height > 0);
|
||||
|
||||
// Metal's viewport coordinates have (0, 0) at the top-left, but Filament's have (0, 0) at
|
||||
// bottom-left.
|
||||
return {double(rect.left),
|
||||
double(height - rect.bottom - int32_t(rect.height)),
|
||||
double(rect.width), double(rect.height),
|
||||
double(depthRangeNear), double(depthRangeFar)};
|
||||
}
|
||||
|
||||
MTLRegion getRegionFromClientRect(Viewport rect) {
|
||||
const uint32_t height = getAttachmentHeight();
|
||||
const uint32_t height = getAttachmentSize().y;
|
||||
assert_invariant(height > 0);
|
||||
|
||||
// Convert the Filament rect into Metal texture coordinates, taking into account Metal's
|
||||
@@ -301,6 +316,8 @@ public:
|
||||
rect.width, rect.height);
|
||||
}
|
||||
|
||||
math::uint2 getAttachmentSize() noexcept;
|
||||
|
||||
bool isDefaultRenderTarget() const { return defaultRenderTarget; }
|
||||
uint8_t getSamples() const { return samples; }
|
||||
|
||||
@@ -316,8 +333,6 @@ private:
|
||||
static id<MTLTexture> createMultisampledTexture(id<MTLDevice> device, MTLPixelFormat format,
|
||||
uint32_t width, uint32_t height, uint8_t samples);
|
||||
|
||||
uint32_t getAttachmentHeight() noexcept;
|
||||
|
||||
MetalContext* context;
|
||||
bool defaultRenderTarget = false;
|
||||
uint8_t samples = 1;
|
||||
@@ -325,7 +340,7 @@ private:
|
||||
Attachment color[MRT::MAX_SUPPORTED_RENDER_TARGET_COUNT] = {};
|
||||
Attachment depth = {};
|
||||
Attachment stencil = {};
|
||||
uint32_t attachmentHeight = 0;
|
||||
math::uint2 attachmentSize = {};
|
||||
};
|
||||
|
||||
// MetalFence is used to implement both Fences and Syncs.
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
#include <utils/trap.h>
|
||||
#include <utils/debug.h>
|
||||
|
||||
#include <math/vec2.h>
|
||||
#include <math/scalar.h>
|
||||
|
||||
#include <math.h>
|
||||
@@ -790,8 +789,8 @@ MetalRenderTarget::MetalRenderTarget(MetalContext* context, uint32_t width, uint
|
||||
uint8_t samples, Attachment colorAttachments[MRT::MAX_SUPPORTED_RENDER_TARGET_COUNT],
|
||||
Attachment depthAttachment, Attachment stencilAttachment) :
|
||||
HwRenderTarget(width, height), context(context), samples(samples) {
|
||||
UTILS_UNUSED_IN_RELEASE math::vec2<uint32_t> tmin = {std::numeric_limits<size_t>::max()};
|
||||
UTILS_UNUSED_IN_RELEASE math::vec2<uint32_t> tmax = {0};
|
||||
math::uint2 tmin = {std::numeric_limits<uint32_t>::max()};
|
||||
UTILS_UNUSED_IN_RELEASE math::uint2 tmax = {0};
|
||||
UTILS_UNUSED_IN_RELEASE size_t attachmentCount = 0;
|
||||
|
||||
for (size_t i = 0; i < MRT::MAX_SUPPORTED_RENDER_TARGET_COUNT; i++) {
|
||||
@@ -880,9 +879,9 @@ MetalRenderTarget::MetalRenderTarget(MetalContext* context, uint32_t width, uint
|
||||
// The render target dimensions must be less than or equal to the attachment size.
|
||||
assert_invariant(width <= tmin.x && height <= tmin.y);
|
||||
|
||||
// Store the height of all attachments. We'll use this when converting from Filament's
|
||||
// Store the width/height of all attachments. We'll use this when converting from Filament's
|
||||
// coordinate system to Metal's.
|
||||
attachmentHeight = tmin.y;
|
||||
attachmentSize = tmin;
|
||||
}
|
||||
|
||||
void MetalRenderTarget::setUpRenderPassAttachments(MTLRenderPassDescriptor* descriptor,
|
||||
@@ -1065,15 +1064,16 @@ id<MTLTexture> MetalRenderTarget::createMultisampledTexture(id<MTLDevice> device
|
||||
return [device newTextureWithDescriptor:descriptor];
|
||||
}
|
||||
|
||||
uint32_t MetalRenderTarget::getAttachmentHeight() noexcept {
|
||||
math::uint2 MetalRenderTarget::getAttachmentSize() noexcept {
|
||||
if (defaultRenderTarget) {
|
||||
// The default render target always has a color attachment.
|
||||
auto colorAttachment = getDrawColorAttachment(0);
|
||||
assert_invariant(colorAttachment);
|
||||
return colorAttachment.getTexture().height;
|
||||
id<MTLTexture> t = colorAttachment.getTexture();
|
||||
return {t.width, t.height};
|
||||
}
|
||||
assert_invariant(attachmentHeight > 0);
|
||||
return attachmentHeight;
|
||||
assert_invariant(attachmentSize.x > 0 && attachmentSize.y > 0);
|
||||
return attachmentSize;
|
||||
}
|
||||
|
||||
MetalFence::MetalFence(MetalContext& context) : context(context), value(context.signalId++) { }
|
||||
|
||||
@@ -164,4 +164,102 @@ TEST_F(BackendTest, ScissorViewportRegion) {
|
||||
getDriver().purge();
|
||||
}
|
||||
|
||||
// Verify that a negative Viewport origin works with scissor.
|
||||
TEST_F(BackendTest, ScissorViewportEdgeCases) {
|
||||
auto& api = getDriverApi();
|
||||
|
||||
api.startCapture(0);
|
||||
|
||||
// The test is executed within this block scope to force destructors to run before
|
||||
// executeCommands().
|
||||
{
|
||||
// Create a SwapChain and make it current. We don't really use it so the res doesn't matter.
|
||||
auto swapChain = api.createSwapChainHeadless(256, 256, 0);
|
||||
api.makeCurrent(swapChain, swapChain);
|
||||
|
||||
// Create a program.
|
||||
ShaderGenerator shaderGen(triangleVs, triangleFs, sBackend, sIsMobilePlatform);
|
||||
Program p = shaderGen.getProgram(api);
|
||||
ProgramHandle program = api.createProgram(std::move(p));
|
||||
|
||||
// Create a source color textures.
|
||||
Handle<HwTexture> srcTexture = api.createTexture(SamplerType::SAMPLER_2D, 1,
|
||||
TextureFormat::RGBA8, 1, 512, 512, 1,
|
||||
TextureUsage::SAMPLEABLE | TextureUsage::COLOR_ATTACHMENT);
|
||||
|
||||
// Render into the bottom-left quarter of the texture, checking 3 special cases.
|
||||
// 1. negative viewport left/bottom
|
||||
Viewport bottomLeftViewport = {
|
||||
.left = -64,
|
||||
.bottom = -64,
|
||||
.width = 256,
|
||||
.height = 256
|
||||
};
|
||||
// 2. viewport that extends beyond the top of the render target.
|
||||
Viewport topLeftViewport = {
|
||||
.left = -64,
|
||||
.bottom = 320,
|
||||
.width = 256,
|
||||
.height = 256
|
||||
};
|
||||
// 3. A scissor rect > render target size.
|
||||
// This is precisely what Filament does to essentially convey "no scissor".
|
||||
Viewport scissor = {0, 0, (uint32_t)std::numeric_limits<int32_t>::max(),
|
||||
(uint32_t)std::numeric_limits<int32_t>::max()};
|
||||
|
||||
Handle<HwRenderTarget> renderTarget = api.createRenderTarget(
|
||||
TargetBufferFlags::COLOR, 512, 512, 1,
|
||||
{srcTexture, 0, 0}, {}, {});
|
||||
|
||||
TrianglePrimitive triangle(api);
|
||||
|
||||
// Render a white triangle over blue.
|
||||
RenderPassParams params = {};
|
||||
params.flags.clear = TargetBufferFlags::COLOR0;
|
||||
params.viewport = bottomLeftViewport;
|
||||
params.clearColor = math::float4(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
params.flags.discardStart = TargetBufferFlags::ALL;
|
||||
params.flags.discardEnd = TargetBufferFlags::NONE;
|
||||
|
||||
PipelineState ps = {};
|
||||
ps.program = program;
|
||||
ps.rasterState.colorWrite = true;
|
||||
ps.rasterState.depthWrite = false;
|
||||
ps.scissor = scissor;
|
||||
|
||||
api.makeCurrent(swapChain, swapChain);
|
||||
api.beginFrame(0, 0);
|
||||
|
||||
api.beginRenderPass(renderTarget, params);
|
||||
api.draw(ps, triangle.getRenderPrimitive(), 1);
|
||||
api.endRenderPass();
|
||||
|
||||
params.viewport = topLeftViewport;
|
||||
params.flags.clear = TargetBufferFlags::NONE;
|
||||
params.flags.discardStart = TargetBufferFlags::NONE;
|
||||
api.beginRenderPass(renderTarget, params);
|
||||
api.draw(ps, triangle.getRenderPrimitive(), 1);
|
||||
api.endRenderPass();
|
||||
|
||||
readPixelsAndAssertHash(
|
||||
"ScissorViewportEdgeCases", 512, 512, renderTarget, 0x6BF00F31, true);
|
||||
|
||||
api.commit(swapChain);
|
||||
api.endFrame(0);
|
||||
|
||||
api.stopCapture(0);
|
||||
|
||||
// Cleanup.
|
||||
api.destroyTexture(srcTexture);
|
||||
api.destroySwapChain(swapChain);
|
||||
api.destroyRenderTarget(renderTarget);
|
||||
}
|
||||
|
||||
// Wait for the ReadPixels result to come back.
|
||||
api.finish();
|
||||
|
||||
executeCommands();
|
||||
getDriver().purge();
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
|
||||
Reference in New Issue
Block a user