Skip to content

Commit

Permalink
Fix, Metal setMinMaxLevel called between render passes (#7175)
Browse files Browse the repository at this point in the history
  • Loading branch information
bejado authored Sep 15, 2023
1 parent 373c571 commit 0b97de4
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 52 deletions.
66 changes: 30 additions & 36 deletions filament/backend/src/metal/MetalDriver.mm
Original file line number Diff line number Diff line change
Expand Up @@ -911,19 +911,15 @@
// textures, which is required by Metal.
for (size_t s = 0; s < data.size / sizeof(SamplerDescriptor); s++) {
if (!samplers[s].t) {
// Assign a default texture / sampler to empty slots.
// Assign a default sampler to empty slots.
// Metal requires all samplers referenced in shaders to be bound.
id<MTLTexture> empty = getOrCreateEmptyTexture(mContext);
sb->setFinalizedTexture(s, empty);

// An empty texture will be assigned inside finalizeSamplerGroup.
id<MTLSamplerState> sampler = mContext->samplerStateCache.getOrCreateState({});
sb->setFinalizedSampler(s, sampler);

continue;
}

// First, bind the sampler state. We always know the full sampler state at
// updateSamplerGroup time.
// Bind the sampler state. We always know the full sampler state at updateSamplerGroup time.
SamplerState samplerState {
.samplerParams = samplers[s].s,
};
Expand Down Expand Up @@ -1379,59 +1375,57 @@
}
#endif

utils::FixedCapacityVector<id<MTLTexture>> newTextures(samplerGroup->size, nil);
for (size_t binding = 0; binding < samplerGroup->size; binding++) {
auto [th, t] = samplerGroup->getFinalizedTexture(binding);

// This may be an external texture, in which case we can't cache the id<MTLTexture>, we
// need to refetch it in case the external image has changed.
bool isExternalImage = false;
if (th) {
auto* texture = handle_cast<MetalTexture>(th);
isExternalImage = texture->target == SamplerType::SAMPLER_EXTERNAL;
}
auto [th, _] = samplerGroup->getFinalizedTexture(binding);

// If t is non-nil, then we've already finalized this texture.
if (t && !isExternalImage) {
if (!th) {
// Bind an empty texture.
newTextures[binding] = getOrCreateEmptyTexture(mContext);
continue;
}

// It's possible that some texture handles are null, but we should have already handled
// these inside updateSamplerGroup by binding an "empty" texture.
assert_invariant(th);
auto* texture = handle_cast<MetalTexture>(th);

// Determine if this SamplerGroup needs mutation.
// External images
if (texture->target == SamplerType::SAMPLER_EXTERNAL) {
if (texture->externalImage.isValid()) {
id<MTLTexture> mtlTexture = texture->externalImage.getMetalTextureForDraw();
assert_invariant(mtlTexture);
newTextures[binding] = mtlTexture;
} else {
// Bind an empty texture.
newTextures[binding] = getOrCreateEmptyTexture(mContext);
}
continue;
}

newTextures[binding] = texture->getMtlTextureForRead();
}

if (!std::equal(newTextures.begin(), newTextures.end(), samplerGroup->textures.begin())) {
// One or more of the id<MTLTexture>s has changed.
// First, determine if this SamplerGroup needs mutation.
// We can't just simply mutate the SamplerGroup, since it could currently be in use by the
// GPU from a prior render pass.
// If the SamplerGroup does need mutation, then there's two cases:
// 1. The SamplerGroup has not been finalized yet (which means it has not yet been used in a
// draw call). We're free to mutate it.
// 2. The SamplerGroup is finalized. We must call mutate(), which will create a new argument
// buffer that we can then mutate freely.
// TODO: don't just always call mutate, check to see if the texture is actually different.

if (samplerGroup->isFinalized()) {
samplerGroup->mutate(cmdBuffer);
}

// External images
if (texture->target == SamplerType::SAMPLER_EXTERNAL) {
if (texture->externalImage.isValid()) {
id<MTLTexture> mtlTexture = texture->externalImage.getMetalTextureForDraw();
assert_invariant(mtlTexture);
samplerGroup->setFinalizedTexture(binding, mtlTexture);
} else {
// Bind an empty texture.
samplerGroup->setFinalizedTexture(binding, getOrCreateEmptyTexture(mContext));
}
continue;
for (size_t binding = 0; binding < samplerGroup->size; binding++) {
samplerGroup->setFinalizedTexture(binding, newTextures[binding]);
}

samplerGroup->setFinalizedTexture(binding, texture->getMtlTextureForRead());
samplerGroup->finalize();
}

samplerGroup->finalize();

// At this point, all the id<MTLTextures> should be set to valid textures. Some of them will be
// the "empty" texture. Per Apple documentation, the useResource method must be called once per
// render pass.
Expand Down
106 changes: 90 additions & 16 deletions filament/backend/test/test_MipLevels.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,18 @@ void main() {
}
)");

std::string whiteFragment (R"(#version 450 core
layout(location = 0) out vec4 fragColor;
layout(location = 0) in vec2 uv;
layout(location = 0, set = 1) uniform sampler2D backend_test_sib_tex;
void main() {
fragColor = vec4(1.0);
}
)");

}

namespace test {
Expand All @@ -70,17 +82,30 @@ TEST_F(BackendTest, SetMinMaxLevel) {
auto swapChain = createSwapChain();
api.makeCurrent(swapChain, swapChain);

// Create a program that draws only white.
Handle<HwProgram> whiteProgram;
{
ShaderGenerator shaderGen(vertex, whiteFragment, sBackend, sIsMobilePlatform);
Program p = shaderGen.getProgram(api);
Program::Sampler sampler{utils::CString("backend_test_sib_tex"), 0};
p.setSamplerGroup(0, ShaderStageFlags::FRAGMENT, &sampler, 1);
whiteProgram = api.createProgram(std::move(p));
}

// Create a program that samples a texture.
SamplerInterfaceBlock sib = filament::SamplerInterfaceBlock::Builder()
.name("backend_test_sib")
.stageFlags(backend::ShaderStageFlags::FRAGMENT)
.add( {{"tex", SamplerType::SAMPLER_2D, SamplerFormat::FLOAT, Precision::HIGH }} )
.build();
ShaderGenerator shaderGen(vertex, fragment, sBackend, sIsMobilePlatform, &sib);
Program p = shaderGen.getProgram(api);
Program::Sampler sampler { utils::CString("backend_test_sib_tex"), 0 };
p.setSamplerGroup(0, ShaderStageFlags::FRAGMENT, &sampler, 1);
auto program = api.createProgram(std::move(p));
Handle<HwProgram> textureProgram;
{
SamplerInterfaceBlock sib = filament::SamplerInterfaceBlock::Builder()
.name("backend_test_sib")
.stageFlags(backend::ShaderStageFlags::FRAGMENT)
.add( {{"tex", SamplerType::SAMPLER_2D, SamplerFormat::FLOAT, Precision::HIGH }} )
.build();
ShaderGenerator shaderGen(vertex, fragment, sBackend, sIsMobilePlatform, &sib);
Program p = shaderGen.getProgram(api);
Program::Sampler sampler{utils::CString("backend_test_sib_tex"), 0};
p.setSamplerGroup(0, ShaderStageFlags::FRAGMENT, &sampler, 1);
textureProgram = api.createProgram(std::move(p));
}

// Create a texture that has 4 mip levels. Each level is a different color.
// Level 0: 128x128 (red)
Expand All @@ -91,7 +116,7 @@ TEST_F(BackendTest, SetMinMaxLevel) {
const size_t kMipLevels = 4;
Handle<HwTexture> texture = api.createTexture(SamplerType::SAMPLER_2D, kMipLevels,
TextureFormat::RGBA8, 1, kTextureSize, kTextureSize, 1,
TextureUsage::SAMPLEABLE | TextureUsage::UPLOADABLE);
TextureUsage::SAMPLEABLE | TextureUsage::COLOR_ATTACHMENT | TextureUsage::UPLOADABLE);

// Create image data.
auto pixelFormat = PixelDataFormat::RGBA;
Expand All @@ -116,8 +141,37 @@ TEST_F(BackendTest, SetMinMaxLevel) {
texture, l, 0, 0, 0, mipSize, mipSize, 1, std::move(descriptor));
}

TrianglePrimitive triangle(api);

api.beginFrame(0, 0);

// We set the base mip to 1, and the max mip to 3
// Level 0: 128x128 (red)
// Level 1: 64x64 (green) <-- base
// Level 2: 32x32 (blue) <--- white triangle rendered
// Level 3: 16x16 (yellow) <-- max
api.setMinMaxLevels(texture, 1, 3);

// Render a white triangle into level 2.
// We specify mip level 2, because minMaxLevels has no effect when rendering into a texture.
Handle<HwRenderTarget> renderTarget = api.createRenderTarget(
TargetBufferFlags::COLOR, 32, 32, 1,
{texture, 2 /* level */, 0 /* layer */}, {}, {});
{
RenderPassParams params = {};
fullViewport(params);
params.flags.clear = TargetBufferFlags::NONE;
params.flags.discardStart = TargetBufferFlags::NONE;
params.flags.discardEnd = TargetBufferFlags::NONE;
PipelineState ps = {};
ps.program = whiteProgram;
ps.rasterState.colorWrite = true;
ps.rasterState.depthWrite = false;
api.beginRenderPass(renderTarget, params);
api.draw(ps, triangle.getRenderPrimitive(), 1);
api.endRenderPass();
}

backend::Handle<HwRenderTarget> defaultRenderTarget = api.createDefaultRenderTarget(0);

RenderPassParams params = {};
Expand All @@ -129,14 +183,12 @@ TEST_F(BackendTest, SetMinMaxLevel) {

PipelineState state;
state.scissor = params.viewport;
state.program = program;
state.program = textureProgram;
state.rasterState.colorWrite = true;
state.rasterState.depthWrite = false;
state.rasterState.depthFunc = SamplerCompareFunc::A;
state.rasterState.culling = CullingMode::NONE;

api.beginFrame(0, 0);

SamplerGroup samplers(1);
SamplerParams samplerParams {};
samplerParams.filterMag = SamplerMagFilter::NEAREST;
Expand All @@ -147,8 +199,26 @@ TEST_F(BackendTest, SetMinMaxLevel) {
api.bindSamplers(0, samplerGroup);

// Render a triangle to the screen, sampling from mip level 1.
// Because the min level is 1, the result color should be blue.
TrianglePrimitive triangle(api);
// Because the min level is 1, the result color should be the white triangle drawn in the
// previous pass.
api.beginRenderPass(defaultRenderTarget, params);
api.draw(state, triangle.getRenderPrimitive(), 1);
api.endRenderPass();

// Adjust the base mip to 2.
// Note that this is done without another call to updateSamplerGroup.
api.setMinMaxLevels(texture, 2, 3);

// Render a second, smaller, triangle, again sampling from mip level 1.
// This triangle should be yellow striped.
static filament::math::float2 vertices[3] = {
{ -0.5, -0.5 },
{ 0.5, -0.5 },
{ -0.5, 0.5 }
};
triangle.updateVertices(vertices);
params.flags.clear = TargetBufferFlags::NONE;
params.flags.discardStart = TargetBufferFlags::NONE;
api.beginRenderPass(defaultRenderTarget, params);
api.draw(state, triangle.getRenderPrimitive(), 1);
api.endRenderPass();
Expand All @@ -160,6 +230,10 @@ TEST_F(BackendTest, SetMinMaxLevel) {

// Cleanup.
api.destroySwapChain(swapChain);
api.destroyRenderTarget(renderTarget);
api.destroyTexture(texture);
api.destroyProgram(whiteProgram);
api.destroyProgram(textureProgram);
}

api.finish();
Expand Down

0 comments on commit 0b97de4

Please sign in to comment.