test(step097): add A1 renderer regression entry points

This commit is contained in:
tei
2026-09-26 09:15:10 +09:00
parent be5b9080ed
commit d0ced72351
7 changed files with 422 additions and 28 deletions

View File

@@ -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<Color>&,
const std::optional<float>&,
const std::optional<int32_t>&);
} // namespace detail
class RenderPass final : public gfx::RenderPass {
public:
RenderPass(CommandEncoder&, const char* name, const gfx::RenderPassDescriptor&);

View File

@@ -55,6 +55,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
private var navSeaPbfLayerVisibility = emptyList<NavSeaPbfLayerVisibility>()
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"

View File

@@ -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))
}

View File

@@ -11,6 +11,39 @@
namespace mbgl {
namespace mtl {
detail::ClearedAttachments detail::applyRenderPassClears(MTL::RenderPassDescriptor& rpd,
const std::optional<Color>& clearColor,
const std::optional<float>& clearDepth,
const std::optional<int32_t>& 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<uint32_t>(*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<uint32_t>(*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");

View File

@@ -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
$<$<BOOL:${MLN_WITH_WEBGPU}>:${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(

View File

@@ -0,0 +1,101 @@
#if MLN_RENDER_BACKEND_METAL
#include <mbgl/mtl/context.hpp>
#include <mbgl/mtl/offscreen_texture.hpp>
#include <mbgl/mtl/render_pass.hpp>
#include <mbgl/mtl/renderable_resource.hpp>
#include <mbgl/mtl/renderer_backend.hpp>
#include <Foundation/Foundation.hpp>
#include <Metal/Metal.hpp>
#include <gtest/gtest.h>
#include <cstdlib>
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<mtl::RenderableResource>();
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<mtl::RenderableResource>();
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

View File

@@ -0,0 +1,110 @@
#include <mbgl/actor/scheduler.hpp>
#include <mbgl/gfx/uniform_buffer.hpp>
#include <mbgl/renderer/layer_group.hpp>
#include <mbgl/renderer/render_orchestrator.hpp>
#include <mbgl/renderer/renderer_observer.hpp>
#include <mbgl/renderer/retained_underlay_cache.hpp>
#include <gtest/gtest.h>
#include <stdexcept>
using namespace mbgl;
namespace {
class TestUniformBuffers final : public gfx::UniformBufferArray {
public:
void bind(gfx::RenderPass&) override {}
protected:
std::unique_ptr<gfx::UniformBuffer> 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<TestLayerGroup>(0)));
ASSERT_TRUE(orchestrator.addLayerGroup(std::make_shared<TestLayerGroup>(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<TestLayerGroup>(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);
}