From d0ced7235172f2f15d0de4f72cbeb8f5afa3ae88 Mon Sep 17 00:00:00 2001 From: tei Date: Sat, 26 Sep 2026 09:15:10 +0900 Subject: [PATCH] test(step097): add A1 renderer regression entry points --- include/mbgl/mtl/render_pass.hpp | 13 +++ .../ParticleCoreLayerValidationActivity.kt | 89 +++++++++++++- .../particle/Step097StencilFixture.kt | 76 ++++++++++++ src/mbgl/mtl/render_pass.cpp | 56 +++++---- test/CMakeLists.txt | 5 + .../mtl_retained_attachments.test.cpp | 101 ++++++++++++++++ .../render_orchestrator_error.test.cpp | 110 ++++++++++++++++++ 7 files changed, 422 insertions(+), 28 deletions(-) create mode 100644 platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step097StencilFixture.kt create mode 100644 test/renderer/mtl_retained_attachments.test.cpp create mode 100644 test/renderer/render_orchestrator_error.test.cpp diff --git a/include/mbgl/mtl/render_pass.hpp b/include/mbgl/mtl/render_pass.hpp index f582b9f32..f8fc01bbe 100644 --- a/include/mbgl/mtl/render_pass.hpp +++ b/include/mbgl/mtl/render_pass.hpp @@ -17,6 +17,19 @@ class BufferResource; class CommandEncoder; class Context; +namespace detail { +struct ClearedAttachments { + bool color = false; + bool depth = false; + bool stencil = false; +}; + +ClearedAttachments applyRenderPassClears(MTL::RenderPassDescriptor&, + const std::optional&, + const std::optional&, + const std::optional&); +} // namespace detail + class RenderPass final : public gfx::RenderPass { public: RenderPass(CommandEncoder&, const char* name, const gfx::RenderPassDescriptor&); diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt index 2ee11d3b0..44ea1c5d8 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt @@ -55,6 +55,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { private var navSeaPbfLayerVisibility = emptyList() private var retainedCacheEnabled = true private var step093Mode = false + private var step097Topology: Step097StencilFixture.Topology? = null private var step093Topology = Step093Topology.BOTTOM_CONTINUOUS_TOP private var step093Variant = Step093Variant.DEFAULT private var step093StartVisible = true @@ -63,8 +64,9 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { private val step093Handler = Handler(Looper.getMainLooper()) private val step093DiagnosticsPoll = object : Runnable { override fun run() { - if (step093Mode && this@ParticleCoreLayerValidationActivity::maplibreMap.isInitialized) { - refreshStep093Diagnostics(log = false) + if ((step093Mode || step097Topology != null) && this@ParticleCoreLayerValidationActivity::maplibreMap.isInitialized) { + if (step097Topology != null) refreshStep097Diagnostics(log = false) + else refreshStep093Diagnostics(log = false) step093Handler.postDelayed(this, STEP093_DIAGNOSTICS_INTERVAL_MS) } } @@ -107,6 +109,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { navSeaPbfRenderEnabled = savedInstanceState?.getBoolean(STATE_NAVSEA_PBF_RENDER_ENABLED, true) ?: true retainedCacheEnabled = savedInstanceState?.getBoolean(STATE_RETAINED_CACHE_ENABLED, true) ?: true step093Mode = savedInstanceState?.getBoolean(STATE_STEP093_MODE, false) ?: false + step097Topology = savedInstanceState?.getString(STATE_STEP097_TOPOLOGY) + ?.let { value -> Step097StencilFixture.Topology.values().firstOrNull { it.name == value } } step093Topology = savedInstanceState?.getString(STATE_STEP093_TOPOLOGY) ?.let { value -> Step093Topology.values().firstOrNull { it.name == value } } ?: Step093Topology.BOTTOM_CONTINUOUS_TOP @@ -145,7 +149,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { updateNavSeaPbfButtonState() updatePbfModeButtonState() updateRetainedCacheButtonState() - if (step093Mode) loadStep093Style(configureBeforeStyle = false) + if (step097Topology != null) loadStep097StencilStyle() + else if (step093Mode) loadStep093Style(configureBeforeStyle = false) else loadValidationStyle(active = active, enabled = enabled, reason = "INITIAL") } @@ -317,7 +322,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { } private fun activateRealWeather(anchor: android.view.View?) { - if (isPbfMatrixMode() || step093Mode) leaveRealWeatherMode() + if (isPbfMatrixMode() || step093Mode || step097Topology != null) leaveRealWeatherMode() realWeatherMenuAnchor = anchor ?: findViewById(R.id.validationMenuRealWeather) if (realWeatherMode) { if (realWeatherTuningMenu == null) showRealWeatherTuning(realWeatherMenuAnchor!!) else dismissRealWeatherTuning() @@ -349,6 +354,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { private fun leaveRealWeatherMode() { step093Mode = false + step097Topology = null step093Handler.removeCallbacks(step093DiagnosticsPoll) dismissRealWeatherTuning() weatherRuntime?.dispose() @@ -446,6 +452,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { menu.add(0, 13, 13, "CAMERA NEXT") menu.add(0, 14, 14, "READ DIAGNOSTICS") menu.add(0, 15, 15, "RESET DEFAULT A") + menu.add(0, 16, 16, "STEP-097 SAME SOURCE STENCIL") setOnMenuItemClickListener { item -> when (item.itemId) { in Step093Topology.values().indices -> applyStep093Scene(Step093Topology.values()[item.itemId]) @@ -455,6 +462,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { 13 -> applyStep093CameraPreset() 14 -> refreshStep093Diagnostics(log = true) 15 -> applyStep093Scene(Step093Topology.BOTTOM_CONTINUOUS_TOP) + 16 -> showStep097StencilMenu(anchor) } true } @@ -462,6 +470,70 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { } } + private fun showStep097StencilMenu(anchor: android.view.View) { + PopupMenu(this, anchor).apply { + Step097StencilFixture.Topology.values().forEachIndexed { index, topology -> + menu.add(0, index, index, "${topology.name} · same source / same tiles") + } + menu.add(0, 10, 10, "REBUILD CURRENT") + menu.add(0, 11, 11, "READ DIAGNOSTICS") + setOnMenuItemClickListener { item -> + when (item.itemId) { + in Step097StencilFixture.Topology.values().indices -> + applyStep097StencilScene(Step097StencilFixture.Topology.values()[item.itemId]) + 10 -> if (step097Topology != null) loadStep097StencilStyle() + 11 -> if (step097Topology != null) refreshStep097Diagnostics(log = true) + } + true + } + show() + } + } + + private fun applyStep097StencilScene(topology: Step097StencilFixture.Topology) { + leaveRealWeatherMode() + step097Topology = topology + retainedCacheEnabled = true + maplibreMap.setRetainedUnderlayEnabled(true) + updateRetainedCacheButtonState() + loadStep097StencilStyle() + } + + private fun loadStep097StencilStyle() { + val topology = step097Topology ?: return + styleReady = false + val epoch = ++styleLoadEpoch + status.text = "STEP-097 · ${topology.name} · LOADING" + val accepted = maplibreMap.setRenderSegmentPlan(Step097StencilFixture.planJson(topology)) + Log.i(TAG, "STEP097_STENCIL_PLAN topology=${topology.name} accepted=$accepted beforeStyle=true") + maplibreMap.setStyle(Style.Builder().fromJson(Step097StencilFixture.styleJson())) { + if (step097Topology != topology || epoch != styleLoadEpoch) return@setStyle + styleReady = true + maplibreMap.moveCamera(CameraUpdateFactory.newCameraPosition( + CameraPosition.Builder().target(LatLng(0.0, 0.0)).zoom(2.0).build() + )) + status.text = statusText() + startStep093DiagnosticsPolling() + refreshStep097Diagnostics(log = true) + Log.i(TAG, "STEP097_STENCIL_READY topology=${topology.name} source=step097-shared-polygon") + } + } + + private fun refreshStep097Diagnostics(log: Boolean) { + val topology = step097Topology ?: return + val snapshot = JSONObject(maplibreMap.getRenderSegmentDiagnostics()) + diagnostics.text = buildString { + append("STEP-097 ${topology.name} valid=${snapshot.optBoolean("valid")} fallback=${snapshot.optBoolean("fallback")} topology=${snapshot.optLong("topologyRevision")} reason=${snapshot.optString("lastReason")}\n") + val segments = snapshot.optJSONArray("segments") + for (index in 0 until (segments?.length() ?: 0)) { + val segment = segments?.optJSONObject(index) ?: continue + val cache = segment.optJSONObject("cache") + append("$index ${segment.optString("strategy")} ${cache?.optString("cacheState")} build=${cache?.optLong("build")} reuse=${cache?.optLong("reuse")}\n") + } + } + if (log) Log.i(TAG, "STEP097_STENCIL_DIAGNOSTICS ${diagnostics.text.toString().replace('\n', ' ')}") + } + private fun showStep093MutationMenu(anchor: android.view.View) { val actions = arrayOf( "CONTINUOUS ADD", "CONTINUOUS REORDER", "BOTTOM ADD", "BOTTOM REORDER", @@ -845,7 +917,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { } private fun enterPbfMatrix() { - if (realWeatherMode || step093Mode) leaveRealWeatherMode() + if (realWeatherMode || step093Mode || step097Topology != null) leaveRealWeatherMode() if (!isPbfMatrixMode()) { preRealWeatherVisuals = ParticleVisuals(particleCount, lineWidth, particleLength, particleOpacity, trailOpacity, trailFade) particleCount = REAL_WEATHER_PARTICLE_COUNT @@ -1558,6 +1630,9 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { } private fun statusText(): String { + step097Topology?.let { topology -> + return "STEP-097 · ${topology.name} · SAME SOURCE STENCIL · CACHE ${if (retainedCacheEnabled) "ON" else "OFF"}" + } if (step093Mode) { return "STEP-093 · ${step093Topology.label} · ${step093Variant.name} · " + "CACHE ${if (retainedCacheEnabled) "ON" else "OFF"}" @@ -1769,7 +1844,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { """.trimIndent() } - override fun onStart() { super.onStart(); mapView.onStart(); if (step093Mode) startStep093DiagnosticsPolling() } + override fun onStart() { super.onStart(); mapView.onStart(); if (step093Mode || step097Topology != null) startStep093DiagnosticsPolling() } override fun onResume() { super.onResume(); mapView.onResume() } override fun onPause() { mapView.onPause(); super.onPause() } override fun onStop() { step093Handler.removeCallbacks(step093DiagnosticsPoll); mapView.onStop(); super.onStop() } @@ -1799,6 +1874,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { outState.putBoolean(STATE_NAVSEA_PBF_RENDER_ENABLED, navSeaPbfRenderEnabled) outState.putBoolean(STATE_RETAINED_CACHE_ENABLED, retainedCacheEnabled) outState.putBoolean(STATE_STEP093_MODE, step093Mode) + outState.putString(STATE_STEP097_TOPOLOGY, step097Topology?.name) outState.putString(STATE_STEP093_TOPOLOGY, step093Topology.name) outState.putString(STATE_STEP093_VARIANT, step093Variant.name) super.onSaveInstanceState(outState) @@ -1835,6 +1911,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { private const val STATE_NAVSEA_PBF_RENDER_ENABLED = "particle_validation.navsea_pbf_render_enabled" private const val STATE_RETAINED_CACHE_ENABLED = "particle_validation.retained_cache_enabled" private const val STATE_STEP093_MODE = "particle_validation.step093_mode" + private const val STATE_STEP097_TOPOLOGY = "particle_validation.step097_topology" private const val STATE_STEP093_TOPOLOGY = "particle_validation.step093_topology" private const val STATE_STEP093_VARIANT = "particle_validation.step093_variant" private const val PARTICLE_LAYER_ID = "particle-raster-core" diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step097StencilFixture.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step097StencilFixture.kt new file mode 100644 index 000000000..e845e7eda --- /dev/null +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/Step097StencilFixture.kt @@ -0,0 +1,76 @@ +package org.maplibre.android.testapp.activity.particle + +import org.json.JSONArray +import org.json.JSONObject + +/** Offline same-source tile fixture for retained target stencil regression. */ +internal object Step097StencilFixture { + enum class Topology { RR, RCR, CRR } + + const val FIRST_FILL = "step097-shared-fill-a" + const val SECOND_FILL = "step097-shared-fill-b" + const val THIRD_FILL = "step097-shared-fill-c" + private const val FOURTH_FILL = "step097-shared-fill-d" + private const val SOURCE = "step097-shared-polygon" + + fun styleJson(): String { + val geometry = JSONObject() + .put("type", "Polygon") + .put("coordinates", JSONArray().put( + JSONArray().put(JSONArray().put(-45).put(-35)) + .put(JSONArray().put(45).put(-35)) + .put(JSONArray().put(45).put(35)) + .put(JSONArray().put(-45).put(35)) + .put(JSONArray().put(-45).put(-35)) + )) + val source = JSONObject().put("type", "geojson").put("data", JSONObject() + .put("type", "Feature") + .put("properties", JSONObject()) + .put("geometry", geometry)) + val layers = JSONArray() + .put(JSONObject().put("id", "step097-background").put("type", "background") + .put("paint", JSONObject().put("background-color", "#15243a"))) + .put(fill(FIRST_FILL, "#f94144")) + .put(fill(SECOND_FILL, "#43aa8b")) + .put(fill(THIRD_FILL, "#577590")) + .put(fill(FOURTH_FILL, "#f9c74f")) + return JSONObject() + .put("version", 8) + .put("name", "STEP-097 same-source stencil fixture") + .put("sources", JSONObject().put(SOURCE, source)) + .put("layers", layers) + .toString() + } + + fun planJson(topology: Topology): String { + val segments = JSONArray() + fun add(strategy: String, start: String? = null) { + val segment = JSONObject().put("strategy", strategy) + if (start != null) segment.put("startLayerId", start) + segments.put(segment) + } + when (topology) { + Topology.RR -> { + add("RETAINED") + add("RETAINED", SECOND_FILL) + } + Topology.RCR -> { + add("RETAINED") + add("CONTINUOUS", SECOND_FILL) + add("RETAINED", THIRD_FILL) + } + Topology.CRR -> { + add("CONTINUOUS") + add("RETAINED", SECOND_FILL) + add("RETAINED", THIRD_FILL) + } + } + return JSONObject().put("schemaVersion", 1).put("segments", segments).toString() + } + + private fun fill(id: String, color: String): JSONObject = JSONObject() + .put("id", id) + .put("type", "fill") + .put("source", SOURCE) + .put("paint", JSONObject().put("fill-color", color).put("fill-opacity", 0.55)) +} diff --git a/src/mbgl/mtl/render_pass.cpp b/src/mbgl/mtl/render_pass.cpp index 9b15cb50c..6bc28a4ab 100644 --- a/src/mbgl/mtl/render_pass.cpp +++ b/src/mbgl/mtl/render_pass.cpp @@ -11,6 +11,39 @@ namespace mbgl { namespace mtl { +detail::ClearedAttachments detail::applyRenderPassClears(MTL::RenderPassDescriptor& rpd, + const std::optional& clearColor, + const std::optional& clearDepth, + const std::optional& clearStencil) { + ClearedAttachments result; + if (clearColor) { + if (auto* target = rpd.colorAttachments()->object(0); target && target->texture()) { + const auto& c = *clearColor; + target->setLoadAction(MTL::LoadActionClear); + target->setClearColor(MTL::ClearColor::Make(c.r, c.g, c.b, c.a)); + target->setStoreAction(MTL::StoreActionStore); + result.color = true; + } + } + if (clearDepth) { + if (auto* target = rpd.depthAttachment(); target && target->texture()) { + target->setLoadAction(MTL::LoadActionClear); + target->setClearDepth(*clearDepth); + target->setStoreAction(MTL::StoreActionStore); + result.depth = true; + } + } + if (clearStencil) { + if (auto* target = rpd.stencilAttachment(); target && target->texture()) { + target->setLoadAction(MTL::LoadActionClear); + target->setClearStencil(static_cast(*clearStencil)); + target->setStoreAction(MTL::StoreActionStore); + result.stencil = true; + } + } + return result; +} + RenderPass::RenderPass(CommandEncoder& commandEncoder_, const char* name, const gfx::RenderPassDescriptor& descriptor) : descriptor(descriptor), commandEncoder(commandEncoder_) { @@ -22,28 +55,7 @@ RenderPass::RenderPass(CommandEncoder& commandEncoder_, const char* name, const if (const auto& original = resource.getRenderPassDescriptor()) { auto rpd = NS::TransferPtr(original->copy()); if (!rpd) throw std::runtime_error("Metal render pass descriptor copy failed"); - if (descriptor.clearColor) { - if (auto* target = rpd->colorAttachments()->object(0); target && target->texture()) { - const auto& c = *descriptor.clearColor; - target->setLoadAction(MTL::LoadActionClear); - target->setClearColor(MTL::ClearColor::Make(c.r, c.g, c.b, c.a)); - target->setStoreAction(MTL::StoreActionStore); - } - } - if (descriptor.clearDepth) { - if (auto* target = rpd->depthAttachment(); target && target->texture()) { - target->setLoadAction(MTL::LoadActionClear); - target->setClearDepth(*descriptor.clearDepth); - target->setStoreAction(MTL::StoreActionStore); - } - } - if (descriptor.clearStencil) { - if (auto* target = rpd->stencilAttachment(); target && target->texture()) { - target->setLoadAction(MTL::LoadActionClear); - target->setClearStencil(static_cast(*descriptor.clearStencil)); - target->setStoreAction(MTL::StoreActionStore); - } - } + detail::applyRenderPassClears(*rpd, descriptor.clearColor, descriptor.clearDepth, descriptor.clearStencil); encoder = NS::RetainPtr(buffer->renderCommandEncoder(rpd.get())); if (!encoder) throw std::runtime_error("Metal render command encoder unavailable"); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 61511defd..21ad10e66 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -29,6 +29,7 @@ add_library( ${PROJECT_SOURCE_DIR}/test/renderer/pattern_atlas.test.cpp ${PROJECT_SOURCE_DIR}/test/renderer/particle_viewport_reseed.test.cpp ${PROJECT_SOURCE_DIR}/test/renderer/retained_underlay_cache.test.cpp + ${PROJECT_SOURCE_DIR}/test/renderer/render_orchestrator_error.test.cpp ${PROJECT_SOURCE_DIR}/test/renderer/shader_registry.test.cpp $<$:${PROJECT_SOURCE_DIR}/test/renderer/wgsl_preprocessor.test.cpp> ${PROJECT_SOURCE_DIR}/test/sprite/sprite_loader.test.cpp @@ -160,6 +161,10 @@ if(MLN_WITH_OPENGL) ) endif() +if(MLN_WITH_METAL) + target_sources(mbgl-test PRIVATE ${PROJECT_SOURCE_DIR}/test/renderer/mtl_retained_attachments.test.cpp) +endif() + if(MLN_WITH_VULKAN) target_sources( diff --git a/test/renderer/mtl_retained_attachments.test.cpp b/test/renderer/mtl_retained_attachments.test.cpp new file mode 100644 index 000000000..4de698d1c --- /dev/null +++ b/test/renderer/mtl_retained_attachments.test.cpp @@ -0,0 +1,101 @@ +#if MLN_RENDER_BACKEND_METAL + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace mbgl; + +namespace { + +class TestMetalBackend final : public mtl::RendererBackend { +public: + TestMetalBackend() : RendererBackend(gfx::ContextMode::Unique) {} + + gfx::Renderable& getDefaultRenderable() override { std::abort(); } + void updateAssumedState() override {} + +private: + void activate() override {} + void deactivate() override {} +}; + +} // namespace + +TEST(MetalRetainedAttachments, ClearActionsApplyToCombinedDepthStencil) { + auto pool = NS::TransferPtr(NS::AutoreleasePool::alloc()->init()); + auto device = NS::TransferPtr(MTL::CreateSystemDefaultDevice()); + if (!device) GTEST_SKIP() << "Metal device unavailable"; + + auto colorDescriptor = MTL::TextureDescriptor::texture2DDescriptor(MTL::PixelFormatRGBA8Unorm, 4, 4, false); + auto combinedDescriptor = + MTL::TextureDescriptor::texture2DDescriptor(MTL::PixelFormatDepth32Float_Stencil8, 4, 4, false); + colorDescriptor->setUsage(MTL::TextureUsageRenderTarget); + combinedDescriptor->setUsage(MTL::TextureUsageRenderTarget); + auto colorTexture = NS::TransferPtr(device->newTexture(colorDescriptor)); + auto combinedTexture = NS::TransferPtr(device->newTexture(combinedDescriptor)); + if (!combinedTexture) GTEST_SKIP() << "Combined depth-stencil texture unavailable"; + ASSERT_TRUE(colorTexture); + + auto descriptor = NS::TransferPtr(MTL::RenderPassDescriptor::alloc()->init()); + descriptor->colorAttachments()->object(0)->setTexture(colorTexture.get()); + descriptor->depthAttachment()->setTexture(combinedTexture.get()); + descriptor->stencilAttachment()->setTexture(combinedTexture.get()); + + const auto applied = mtl::detail::applyRenderPassClears( + *descriptor, Color{0.2f, 0.3f, 0.4f, 0.5f}, 1.0f, 0); + EXPECT_TRUE(applied.color); + EXPECT_TRUE(applied.depth); + EXPECT_TRUE(applied.stencil); + EXPECT_EQ(descriptor->colorAttachments()->object(0)->loadAction(), MTL::LoadActionClear); + EXPECT_EQ(descriptor->depthAttachment()->loadAction(), MTL::LoadActionClear); + EXPECT_EQ(descriptor->stencilAttachment()->loadAction(), MTL::LoadActionClear); + EXPECT_EQ(descriptor->colorAttachments()->object(0)->storeAction(), MTL::StoreActionStore); + EXPECT_EQ(descriptor->depthAttachment()->storeAction(), MTL::StoreActionStore); + EXPECT_EQ(descriptor->stencilAttachment()->storeAction(), MTL::StoreActionStore); + EXPECT_DOUBLE_EQ(descriptor->depthAttachment()->clearDepth(), 1.0); + EXPECT_EQ(descriptor->stencilAttachment()->clearStencil(), 0u); + EXPECT_EQ(descriptor->depthAttachment()->texture(), descriptor->stencilAttachment()->texture()); +} + +TEST(MetalRetainedAttachments, OffscreenBindsRequestedAttachmentsAndSkipsAbsentOnes) { + auto pool = NS::TransferPtr(NS::AutoreleasePool::alloc()->init()); + TestMetalBackend backend; + mtl::Context context{backend}; + + mtl::OffscreenTexture retained(context, {4, 4}, gfx::TextureChannelDataType::UnsignedByte, true, true); + ASSERT_TRUE(retained.isRenderable()); + auto& retainedResource = retained.getResource(); + retainedResource.bind(); + const auto& retainedDescriptor = retainedResource.getRenderPassDescriptor(); + ASSERT_TRUE(retainedDescriptor); + ASSERT_NE(retainedDescriptor->colorAttachments()->object(0)->texture(), nullptr); + ASSERT_NE(retainedDescriptor->depthAttachment()->texture(), nullptr); + ASSERT_NE(retainedDescriptor->stencilAttachment()->texture(), nullptr); +#if TARGET_OS_SIMULATOR || defined(__x86_64__) + EXPECT_EQ(retainedDescriptor->depthAttachment()->texture(), retainedDescriptor->stencilAttachment()->texture()); +#else + EXPECT_NE(retainedDescriptor->depthAttachment()->texture(), retainedDescriptor->stencilAttachment()->texture()); +#endif + + mtl::OffscreenTexture colorOnly(context, {4, 4}, gfx::TextureChannelDataType::UnsignedByte, false, false); + ASSERT_TRUE(colorOnly.isRenderable()); + auto& colorResource = colorOnly.getResource(); + colorResource.bind(); + const auto& colorDescriptor = colorResource.getRenderPassDescriptor(); + ASSERT_TRUE(colorDescriptor); + EXPECT_EQ(colorDescriptor->depthAttachment()->texture(), nullptr); + EXPECT_EQ(colorDescriptor->stencilAttachment()->texture(), nullptr); + const auto applied = mtl::detail::applyRenderPassClears(*colorDescriptor, std::nullopt, 1.0f, 0); + EXPECT_FALSE(applied.depth); + EXPECT_FALSE(applied.stencil); +} + +#endif // MLN_RENDER_BACKEND_METAL diff --git a/test/renderer/render_orchestrator_error.test.cpp b/test/renderer/render_orchestrator_error.test.cpp new file mode 100644 index 000000000..cbd73c4cc --- /dev/null +++ b/test/renderer/render_orchestrator_error.test.cpp @@ -0,0 +1,110 @@ +#include +#include +#include +#include +#include +#include + +#include + +#include + +using namespace mbgl; + +namespace { + +class TestUniformBuffers final : public gfx::UniformBufferArray { +public: + void bind(gfx::RenderPass&) override {} +protected: + std::unique_ptr copy(const gfx::UniformBuffer&) override { return nullptr; } +}; + +class TestLayerGroup final : public LayerGroup { +public: + explicit TestLayerGroup(int32_t index) + : LayerGroup(index, 0, "step097-error-group") {} + + const gfx::UniformBufferArray& getUniformBuffers() const override { return buffers; } + gfx::UniformBufferArray& mutableUniformBuffers() override { return buffers; } + +private: + TestUniformBuffers buffers; +}; + +class ErrorObserver final : public RendererObserver { +public: + void onRenderError(std::exception_ptr error) override { + ++errors; + try { + std::rethrow_exception(error); + } catch (const std::runtime_error& e) { + message = e.what(); + } + } + + int errors = 0; + std::string message; +}; + +} // namespace + +TEST(RenderOrchestratorError, RangeVisitorReportsOpaqueAndTranslucentFailures) { + TaggedScheduler scheduler{Scheduler::GetBackground(), {}}; + RenderOrchestrator orchestrator(false, scheduler, std::nullopt); + ErrorObserver observer; + orchestrator.setObserver(&observer); + ASSERT_TRUE(orchestrator.addLayerGroup(std::make_shared(0))); + ASSERT_TRUE(orchestrator.addLayerGroup(std::make_shared(1))); + + bool failed = false; + orchestrator.visitLayerGroupsInRangeReversed(0, 2, [](LayerGroupBase&, size_t) { + throw std::runtime_error("opaque failure"); + }, &failed); + EXPECT_TRUE(failed); + EXPECT_EQ(observer.errors, 2); + EXPECT_EQ(observer.message, "opaque failure"); + + failed = false; + observer.errors = 0; + orchestrator.visitLayerGroupsInRange(0, 2, [](LayerGroupBase&, size_t) { + throw std::runtime_error("translucent failure"); + }, &failed); + EXPECT_TRUE(failed); + EXPECT_EQ(observer.errors, 2); + EXPECT_EQ(observer.message, "translucent failure"); + + failed = false; + int visits = 0; + orchestrator.visitLayerGroupsInRange(0, 2, [&](LayerGroupBase&, size_t) { ++visits; }, &failed); + EXPECT_FALSE(failed); + EXPECT_EQ(visits, 2); +} + +TEST(RenderOrchestratorError, FailedRangeCannotReuseAndCleanRetryCanRecover) { + TaggedScheduler scheduler{Scheduler::GetBackground(), {}}; + RenderOrchestrator orchestrator(false, scheduler, std::nullopt); + ErrorObserver observer; + orchestrator.setObserver(&observer); + ASSERT_TRUE(orchestrator.addLayerGroup(std::make_shared(0))); + + RetainedUnderlayCache cache; + RetainedUnderlayKey key{{4, 4}, 0, 1, {}}; + key.cameraMatrix[0] = key.cameraMatrix[5] = key.cameraMatrix[10] = key.cameraMatrix[15] = 1.0; + for (bool opaque : {true, false}) { + EXPECT_EQ(cache.evaluate(key, 1, true), RetainedUnderlayDecision::Build); + bool failed = false; + const auto throwOnRender = [](LayerGroupBase&, size_t) { throw std::runtime_error("render failure"); }; + if (opaque) orchestrator.visitLayerGroupsInRangeReversed(0, 1, throwOnRender, &failed); + else orchestrator.visitLayerGroupsInRange(0, 1, throwOnRender, &failed); + ASSERT_TRUE(failed); + cache.markBuildFailed("layer-render-error"); + EXPECT_FALSE(cache.isValid()); + EXPECT_EQ(cache.evaluate(key, 1, true), RetainedUnderlayDecision::Build); + } + bool failed = false; + orchestrator.visitLayerGroupsInRange(0, 1, [](LayerGroupBase&, size_t) {}, &failed); + ASSERT_FALSE(failed); + cache.markBuildSucceeded(); + EXPECT_EQ(cache.evaluate(key, 1, true), RetainedUnderlayDecision::Reuse); +}