diff --git a/filament/backend/src/metal/MetalDriver.mm b/filament/backend/src/metal/MetalDriver.mm index 9fbba163bcd..d14d9fb900c 100644 --- a/filament/backend/src/metal/MetalDriver.mm +++ b/filament/backend/src/metal/MetalDriver.mm @@ -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 empty = getOrCreateEmptyTexture(mContext); - sb->setFinalizedTexture(s, empty); - + // An empty texture will be assigned inside finalizeSamplerGroup. id 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, }; @@ -1379,28 +1375,38 @@ } #endif + utils::FixedCapacityVector> 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, we - // need to refetch it in case the external image has changed. - bool isExternalImage = false; - if (th) { - auto* texture = handle_cast(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(th); - // Determine if this SamplerGroup needs mutation. + // External images + if (texture->target == SamplerType::SAMPLER_EXTERNAL) { + if (texture->externalImage.isValid()) { + id 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 ids 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: @@ -1408,30 +1414,18 @@ // 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 = 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 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. diff --git a/filament/backend/test/test_MipLevels.cpp b/filament/backend/test/test_MipLevels.cpp index e677e904d5b..a794003ba3f 100644 --- a/filament/backend/test/test_MipLevels.cpp +++ b/filament/backend/test/test_MipLevels.cpp @@ -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 { @@ -70,17 +82,30 @@ TEST_F(BackendTest, SetMinMaxLevel) { auto swapChain = createSwapChain(); api.makeCurrent(swapChain, swapChain); + // Create a program that draws only white. + Handle 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 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) @@ -91,7 +116,7 @@ TEST_F(BackendTest, SetMinMaxLevel) { const size_t kMipLevels = 4; Handle 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; @@ -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 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 defaultRenderTarget = api.createDefaultRenderTarget(0); RenderPassParams params = {}; @@ -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; @@ -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(); @@ -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();