#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace mbgl { namespace { constexpr auto ShaderName = "ParticleGpuValidationShader"; constexpr Size StateSize{16, 16}; constexpr Size MaskSize{16, 16}; std::atomic_uint64_t nextGpuGeneration{0}; std::atomic_uint64_t nextMaskGeneration{0}; std::atomic_uint64_t nextFieldGeneration{0}; struct ParticleSettings { bool active; uint8_t maskMode; uint8_t fixtureMode; bool usesFormalSource; float timeMix; }; ParticleSettings particleSettings(const Immutable& value) { if (value->getTypeInfo() == style::RasterParticleLayer::Impl::staticTypeInfo()) { const auto& layer = static_cast(*value); return {layer.active, layer.maskMode, 0, true, layer.timeMix}; } assert(value->getTypeInfo() == style::ParticleValidationLayer::Impl::staticTypeInfo()); const auto& layer = static_cast(*value); return {layer.active, layer.maskMode, layer.fixtureMode, false, 0.0f}; } enum class ValidationPass : uint8_t { State = 0, Trail = 1, Composite = 2, FrozenComposite = 3, }; const char* slotName(const uint8_t slot) { return slot == 0 ? "A" : "B"; } Size validViewportSize(const Size size) { return {std::max(1, size.width), std::max(1, size.height)}; } const char* fixtureID(const uint8_t mode) { switch (mode) { case 0: return "uniform-east"; case 1: return "uniform-north"; default: return "vortex"; } } std::shared_ptr makeVelocityFieldImage(const particle::ParticleFieldSnapshot& field) { auto image = std::make_shared(Size{field.width, field.height}); for (uint32_t y = 0; y < field.height; ++y) { for (uint32_t x = 0; x < field.width; ++x) { const auto cell = (static_cast(y) * field.width + x) * 2; const auto pixel = (static_cast(y) * field.width + x) * PremultipliedImage::channels; image->data[pixel + 0] = static_cast(std::clamp(field.velocity[cell] * 0.5f + 0.5f, 0.0f, 1.0f) * 255.0f); image->data[pixel + 1] = static_cast( std::clamp(field.velocity[cell + 1] * 0.5f + 0.5f, 0.0f, 1.0f) * 255.0f); image->data[pixel + 2] = 0; image->data[pixel + 3] = field.valid[y * field.width + x] ? 255 : 0; } } return image; } // VALIDATION ONLY: deterministic global fixture with a diagonal coast. Alpha // 255 means OCEAN, alpha 0 means LAND. It is intentionally tiny and is not a // production coastline source. std::shared_ptr makeCoastalMaskFixture() { auto image = std::make_shared(MaskSize); for (uint32_t y = 0; y < MaskSize.height; ++y) { for (uint32_t x = 0; x < MaskSize.width; ++x) { const bool ocean = x > (y / 2 + 3) && !(x > 11 && y < 4); const auto offset = (y * MaskSize.width + x) * PremultipliedImage::channels; image->data[offset + 0] = ocean ? 36 : 90; image->data[offset + 1] = ocean ? 128 : 66; image->data[offset + 2] = ocean ? 188 : 42; image->data[offset + 3] = ocean ? 255 : 0; } } return image; } int16_t wrapForLongitude(const double longitude) { return static_cast(std::floor((longitude + 180.0) / 360.0)); } void removeTargets(const std::vector& targets, UniqueChangeRequestVec& changes) { for (const auto& target : targets) { if (target) { changes.emplace_back(std::make_unique(target)); } } } } // namespace class ParticleGpuResources { public: uint64_t generation = 0; uint64_t committedFrame = 0; uint8_t stateRead = 0; uint8_t trailRead = 0; bool stateSeeded = false; bool trailSeeded = false; bool pending = false; bool bootstrapPending = false; bool contextDestroyed = false; bool lifecycleModeKnown = false; bool lifecycleActive = false; bool formalDisplay = false; bool displayModeLogged = false; bool staticFreezeLogged = false; bool cameraDrawLogged = false; bool worldWrapLogged = false; bool antimeridianLogged = false; uint64_t lastCameraLogFrame = 0; uint64_t maskGeneration = 0; uint64_t fieldGeneration = 0; uint64_t gpuFieldGeneration = 0; uint64_t nextFieldGeneration = 0; uint64_t gpuNextFieldGeneration = 0; uint64_t fieldUploadCount = 0; float timeMix = 0.0f; uint8_t fixtureMode = 255; uint8_t loggedMaskMode = 255; Size trailSize{}; std::array stateTargets; std::array trailTargets; std::vector registeredTargets; gfx::ShaderProgramBasePtr shader; gfx::Texture2DPtr maskTexture; gfx::Texture2DPtr fieldTexture; gfx::Texture2DPtr nextFieldTexture; gfx::Texture2DPtr stateTexture; particle::ParticleFieldSnapshot field; particle::ParticleFieldSnapshot nextField; std::shared_ptr fieldImage; std::shared_ptr nextFieldImage; std::shared_ptr maskFixture; std::shared_ptr> vertices; SegmentVector segments; uint8_t stateWrite() const { return static_cast(1 - stateRead); } uint8_t trailWrite() const { return static_cast(1 - trailRead); } void resetSequence() { committedFrame = 0; stateRead = 0; trailRead = 0; stateSeeded = false; trailSeeded = false; pending = false; bootstrapPending = false; staticFreezeLogged = false; for (const auto& target : stateTargets) { if (target && target->getLayerGroup(0)) { target->getLayerGroup(0)->clearDrawables(); } } for (const auto& target : trailTargets) { if (target && target->getLayerGroup(0)) { target->getLayerGroup(0)->clearDrawables(); } } } void commit() { if (!pending) { return; } stateRead = stateWrite(); trailRead = trailWrite(); stateSeeded = true; trailSeeded = true; if (!bootstrapPending) { ++committedFrame; } bootstrapPending = false; pending = false; } }; namespace { class ParticleGpuValidationTweaker final : public gfx::DrawableTweaker { public: ParticleGpuValidationTweaker(std::shared_ptr state_, ValidationPass pass_, uint8_t readSlot_, uint8_t writeSlot_, gfx::Texture2DPtr source_, gfx::Texture2DPtr target_, gfx::Texture2DPtr mask_, gfx::Texture2DPtr stateTexture_, float simulationStep_, int16_t worldWrap_, uint8_t maskMode_) : state(std::move(state_)), pass(pass_), readSlot(readSlot_), writeSlot(writeSlot_), source(std::move(source_)), target(std::move(target_)), mask(std::move(mask_)), stateTexture(std::move(stateTexture_)), simulationStep(simulationStep_), worldWrap(worldWrap_), maskMode(maskMode_) {} void init(gfx::Drawable&) override {} void execute(gfx::Drawable& drawable, PaintParameters& parameters) override { auto resources = state.lock(); if (parameters.pass != RenderPass::Translucent || !resources || !source) { return; } drawable.setTexture(source, shaders::idParticleGpuValidationPreviousTexture); if (mask) { drawable.setTexture(mask, shaders::idParticleGpuValidationMaskTexture); } if (resources->fieldTexture) { drawable.setTexture(resources->fieldTexture, shaders::idParticleGpuValidationFieldTexture); } if (resources->nextFieldTexture) { drawable.setTexture(resources->nextFieldTexture, shaders::idParticleGpuValidationNextFieldTexture); } if (stateTexture) { drawable.setTexture(stateTexture, shaders::idParticleGpuValidationStateTexture); } const float frame = simulationStep; const float markerX = resources->timeMix; const float markerY = 0.5f + 0.32f * std::cos(frame * 0.053f); // RenderTarget clear is opaque black. State initialization must therefore // be an explicit lifecycle signal, never inferred from sampled alpha. const bool seedState = pass == ValidationPass::State && !resources->stateSeeded; const shaders::ParticleGpuValidationUBO ubo = { .pass = {static_cast(pass), frame, maskMode == 0 ? 0.0f : 1.0f, static_cast(maskMode)}, .marker = {markerX, markerY, 0.035f, 0.965f}, .mask = {static_cast(resources->maskGeneration), static_cast(worldWrap), resources->formalDisplay ? 1.0f : 0.0f, seedState ? 1.0f : 0.0f}, .matrix = util::cast(parameters.matrixForTile({worldWrap, {0, 0, 0}})), }; drawable.mutableUniformBuffers().createOrUpdate(shaders::idParticleGpuValidationUBO, &ubo, parameters.context); if (pass == ValidationPass::Composite || pass == ValidationPass::FrozenComposite) { const auto center = parameters.state.getLatLng(LatLng::Unwrapped); const bool primaryWorldCopy = worldWrap == wrapForLongitude(center.longitude()); if (primaryWorldCopy && !resources->displayModeLogged) { Log::Info(Event::Render, "ParticleRender: displayMode=" + std::string(resources->formalDisplay ? "FORMAL_PARTICLE" : "VALIDATION") + " generation=" + std::to_string(resources->generation) + " compositeTrailRead=" + slotName(readSlot)); resources->displayModeLogged = true; } if (primaryWorldCopy && !resources->cameraDrawLogged) { std::ostringstream message; message << "ParticleValidation30_03: first camera-locked draw maskGeneration=" << resources->maskGeneration << " maskMode=" << static_cast(maskMode) << " center=" << center.latitude() << ',' << center.longitude() << " worldCopy=" << worldWrap << " zoom=" << parameters.state.getZoom() << " bearing=" << parameters.state.getBearing() << " pitch=" << parameters.state.getPitch() << " pixelRatio=" << parameters.pixelRatio; Log::Info(Event::Render, message.str()); resources->cameraDrawLogged = true; } if (primaryWorldCopy && parameters.frameCount >= resources->lastCameraLogFrame + 30) { Log::Info(Event::Render, "ParticleValidation30_03: camera frame=" + std::to_string(parameters.frameCount) + " center=" + std::to_string(center.latitude()) + ',' + std::to_string(center.longitude()) + " zoom=" + std::to_string(parameters.state.getZoom()) + " bearing=" + std::to_string(parameters.state.getBearing()) + " pitch=" + std::to_string(parameters.state.getPitch()) + " viewport=" + std::to_string(parameters.state.getSize().width) + 'x' + std::to_string(parameters.state.getSize().height)); resources->lastCameraLogFrame = parameters.frameCount; } if (primaryWorldCopy && worldWrap != 0 && !resources->worldWrapLogged) { Log::Info(Event::Render, "ParticleValidation30_03: world wrap draw canonical=0/0/0 wrap=" + std::to_string(worldWrap)); resources->worldWrapLogged = true; } if (primaryWorldCopy && std::abs(center.longitude()) >= 170.0 && !resources->antimeridianLogged) { Log::Info(Event::Render, "ParticleValidation30_03: antimeridian camera centerLon=" + std::to_string(center.longitude()) + " canonicalMask=[-180,180]"); resources->antimeridianLogged = true; } if (pass == ValidationPass::FrozenComposite) { if (!resources->staticFreezeLogged) { const auto size = parameters.state.getSize(); std::ostringstream message; message << "ParticleValidation30_02: STATIC frozen composite generation=" << resources->generation << " frame=" << resources->committedFrame << " stateRead=" << slotName(resources->stateRead) << " trailRead=" << slotName(resources->trailRead) << " offscreenPasses=0 swap=false viewport=" << size.width << 'x' << size.height << " zoom=" << parameters.state.getZoom() << " bearing=" << parameters.state.getBearing() << " pitch=" << parameters.state.getPitch(); Log::Info(Event::Render, message.str()); resources->staticFreezeLogged = true; } return; } // The same canonical quad is drawn for adjacent MapLibre world // copies. Only the center copy owns the 30-02 slot commit; copies // are draw-only and must never advance simulation a second time. if (!primaryWorldCopy) { return; } const auto beforeState = resources->stateRead; const auto beforeTrail = resources->trailRead; resources->commit(); if (resources->committedFrame <= 3 || resources->committedFrame % 60 == 0) { const auto size = parameters.state.getSize(); std::ostringstream message; message << "ParticleValidation30_02: commit generation=" << resources->generation << " frame=" << resources->committedFrame << " state=" << slotName(beforeState) << "->" << slotName(resources->stateRead) << " trail=" << slotName(beforeTrail) << "->" << slotName(resources->trailRead) << " order=State,Trail,Composite hazard=SAFE viewport=" << size.width << 'x' << size.height << " zoom=" << parameters.state.getZoom() << " bearing=" << parameters.state.getBearing() << " pitch=" << parameters.state.getPitch(); Log::Info(Event::Render, message.str()); } return; } if (source == target) { Log::Error(Event::Render, "ParticleValidation30_02: HAZARD source equals target generation=" + std::to_string(resources->generation)); return; } if (resources->committedFrame < 3 || (resources->committedFrame + 1) % 60 == 0) { const auto passName = pass == ValidationPass::State ? "State" : "Trail"; Log::Info(Event::Render, "ParticleValidation30_02: pass=" + std::string(passName) + " generation=" + std::to_string(resources->generation) + " frame=" + std::to_string(resources->committedFrame + 1) + " read=" + slotName(readSlot) + " write=" + slotName(writeSlot) + " distinct=true"); } if (pass == ValidationPass::State && (!resources->stateSeeded || resources->committedFrame < 3 || (resources->committedFrame + 1) % 60 == 0)) { Log::Info(Event::Render, "ParticleRender: stateSeed=" + std::string(seedState ? "INITIALIZE" : "CONTINUE") + " generation=" + std::to_string(resources->generation) + " state=" + slotName(readSlot) + "->" + slotName(writeSlot) + " fixture=" + resources->field.fixtureID); } } private: std::weak_ptr state; ValidationPass pass; uint8_t readSlot; uint8_t writeSlot; gfx::Texture2DPtr source; gfx::Texture2DPtr target; gfx::Texture2DPtr mask; gfx::Texture2DPtr stateTexture; float simulationStep; int16_t worldWrap; uint8_t maskMode; }; RenderTargetPtr createValidationTarget(gfx::Context& context, const Size size, const std::string& name, const gfx::TextureFilterType filter) { auto target = context.createRenderTarget(size, gfx::TextureChannelDataType::UnsignedByte); if (!target || !target->getTexture()) { return {}; } target->getTexture()->setSamplerConfiguration( {.filter = filter, .wrapU = gfx::TextureWrapType::Clamp, .wrapV = gfx::TextureWrapType::Clamp}); auto group = context.createLayerGroup(0, 1, name); if (!group || !target->addLayerGroup(std::move(group), true)) { return {}; } return target; } bool buildQuad(gfx::Context& context, const std::shared_ptr& resources, LayerGroup& group, const std::string& name, const gfx::Texture2DPtr& source, const gfx::Texture2DPtr& target, const gfx::Texture2DPtr& mask, const gfx::Texture2DPtr& stateTexture, const float simulationStep, const ValidationPass pass, const uint8_t readSlot, const uint8_t writeSlot, const int16_t worldWrap, const uint8_t maskMode, const bool clearGroup) { if (clearGroup) { group.clearDrawables(); } auto attributes = context.createVertexAttributeArray(); if (const auto& attribute = attributes->set(shaders::idParticleGpuValidationPosVertexAttribute)) { attribute->setSharedRawData(resources->vertices, offsetof(HeatmapTextureLayoutVertex, a1), 0, sizeof(HeatmapTextureLayoutVertex), gfx::AttributeDataType::Short2); } auto builder = context.createDrawableBuilder(name); builder->setShader(resources->shader); builder->setRenderPass(RenderPass::Translucent); builder->setEnableDepth(false); builder->setColorMode(pass == ValidationPass::Composite || pass == ValidationPass::FrozenComposite ? gfx::ColorMode::alphaBlended() : gfx::ColorMode::unblended()); builder->setCullFaceMode(gfx::CullFaceMode::disabled()); builder->setVertexAttributes(std::move(attributes)); builder->setRawVertices({}, resources->vertices->elements(), gfx::AttributeDataType::Short2); builder->setSegments(gfx::Triangles(), RenderStaticData::quadTriangleIndices().vector(), resources->segments.data(), resources->segments.size()); builder->setTexture(source, shaders::idParticleGpuValidationPreviousTexture); if (mask) { builder->setTexture(mask, shaders::idParticleGpuValidationMaskTexture); } if (resources->fieldTexture) { builder->setTexture(resources->fieldTexture, shaders::idParticleGpuValidationFieldTexture); } if (resources->nextFieldTexture) { builder->setTexture(resources->nextFieldTexture, shaders::idParticleGpuValidationNextFieldTexture); } if (stateTexture) { builder->setTexture(stateTexture, shaders::idParticleGpuValidationStateTexture); } builder->addTweaker(std::make_shared( resources, pass, readSlot, writeSlot, source, target, mask, stateTexture, simulationStep, worldWrap, maskMode)); // DrawableBuilder owns the current drawable until flush finalizes it. builder->flush(context); auto drawables = builder->clearDrawables(); if (drawables.size() != 1 || !drawables.front()) { Log::Error(Event::Render, "ParticleValidation30_02: failed to finalize drawable name=" + name); return false; } group.addDrawable(std::move(drawables.front())); return true; } } // namespace RenderRasterParticleLayer::RenderRasterParticleLayer(Immutable impl_) : RenderLayer([&] { const auto settings = particleSettings(impl_); return makeMutable( std::move(impl_), settings.active, settings.maskMode, settings.fixtureMode, settings.timeMix); }()) { formalSource = baseImpl->getTypeInfo() == style::RasterParticleLayer::Impl::staticTypeInfo(); Log::Info(Event::Render, "ParticleRender: RenderLayer create id=" + getID() + " source=" + (formalSource ? baseImpl->source : std::string("validation-only"))); } RenderRasterParticleLayer::~RenderRasterParticleLayer() { if (gpuResources) { Log::Info(Event::Render, "ParticleValidation30_02: resources destroy generation=" + std::to_string(gpuResources->generation) + " contextDestroyed=" + (gpuResources->contextDestroyed ? "true" : "false")); } Log::Info(Event::Render, "ParticleValidation: RenderLayer destroy id=" + getID()); } void RenderRasterParticleLayer::evaluate(const PropertyEvaluationParameters&) { const auto settings = particleSettings(baseImpl); auto properties = makeMutable( baseImpl, settings.active, settings.maskMode, settings.fixtureMode, settings.timeMix); passes = RenderPass::Translucent; properties->renderPasses = mbgl::underlying_type(passes); evaluatedProperties = std::move(properties); Log::Info(Event::Render, "ParticleRender: state=" + std::string(settings.active ? "ACTIVE" : "STATIC") + " maskMode=" + std::to_string(settings.maskMode) + " id=" + getID() + " fixture=" + (formalSource && sourceField ? sourceField->fixtureID : fixtureID(settings.fixtureMode)) + " layerIndex=" + std::to_string(getLayerIndex()) + " timeMix=" + std::to_string(settings.timeMix) + " stage=40-02"); } bool RenderRasterParticleLayer::needsRepaint() const { const auto& properties = static_cast(*evaluatedProperties); return properties.active && baseImpl->visibility != style::VisibilityType::None; } void RenderRasterParticleLayer::markLayerRenderable(bool willRender, UniqueChangeRequestVec& changes) { // RenderOrchestrator calls this while visiting each source. In a style with more // than one source, false can therefore be a transient per-source result, not a // durable particle disable. Persistent State/Trail resources are invalidated // only by their real lifecycle events (field, viewport, context, or layer removal). RenderLayer::markLayerRenderable(willRender, changes); } void RenderRasterParticleLayer::layerRemoved(UniqueChangeRequestVec& changes) { if (gpuResources) { removeTargets(gpuResources->registeredTargets, changes); gpuResources->registeredTargets.clear(); } RenderLayer::layerRemoved(changes); } void RenderRasterParticleLayer::markContextDestroyed() { if (gpuResources) { gpuResources->contextDestroyed = true; Log::Info( Event::Render, "ParticleValidation30_02: context lost discard generation=" + std::to_string(gpuResources->generation) + " stateRead=" + slotName(gpuResources->stateRead) + " trailRead=" + slotName(gpuResources->trailRead)); } } void RenderRasterParticleLayer::prepare(const LayerPrepareParameters& parameters) { if (!formalSource || !parameters.source || parameters.source->baseImpl->type != style::SourceType::RasterArray) { return; } const auto& source = static_cast(*parameters.source); if (source.impl().field) { sourceField = *source.impl().field; sourceNextField = source.impl().nextField ? *source.impl().nextField : *source.impl().field; } } void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders, gfx::Context& context, const TransformState& transformState, const std::shared_ptr&, const RenderTree&, UniqueChangeRequestVec& changes) { if (!layerGroup) { if (auto group = context.createLayerGroup(layerIndex, 1, getID())) { setLayerGroup(std::move(group), changes); } } if (!layerGroup || !isLayerRenderable()) { return; } const auto& properties = static_cast(*evaluatedProperties); const bool active = properties.active; const uint8_t maskMode = properties.maskMode; const uint8_t fixtureMode = properties.fixtureMode; auto resources = gpuResources; if (resources && resources->contextDestroyed) { // VALIDATION ONLY: follow the 30-02 resource-generation rule. No stale // State, Trail, or Mask texture survives a notified context loss. gpuResources.reset(); resources.reset(); } if (!resources) { auto owned = std::make_shared(); owned->generation = ++nextGpuGeneration; owned->formalDisplay = formalSource; owned->shader = context.getGenericShader(shaders, ShaderName); owned->vertices = std::make_shared>( RenderStaticData::heatmapTextureVertices()); owned->segments = RenderStaticData::heatmapTextureSegments(); for (uint8_t i = 0; i < 2; ++i) { owned->stateTargets[i] = createValidationTarget( context, StateSize, getID() + "-state-" + slotName(i), gfx::TextureFilterType::Nearest); } if (!owned->shader || !owned->stateTargets[0] || !owned->stateTargets[1]) { Log::Error(Event::Render, "ParticleValidation30_02: State resource creation failed"); return; } owned->maskFixture = makeCoastalMaskFixture(); owned->maskTexture = context.createTexture2D(); if (!owned->maskFixture || !owned->maskTexture) { Log::Error(Event::Render, "ParticleValidation30_03: mask fixture/resource creation failed"); return; } owned->maskTexture->setImage(owned->maskFixture); owned->maskTexture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Nearest, .wrapU = gfx::TextureWrapType::Clamp, .wrapV = gfx::TextureWrapType::Clamp}); owned->maskGeneration = ++nextMaskGeneration; try { if (formalSource) { if (!sourceField || !sourceNextField) { Log::Error(Event::Render, "ParticleField: raster-array source has no loaded time window"); return; } owned->field = *sourceField; owned->nextField = *sourceNextField; owned->fieldGeneration = owned->field.generation; owned->nextFieldGeneration = owned->nextField.generation; } else { owned->fieldGeneration = ++nextFieldGeneration; owned->field = particle::OfflineFixtureParticleAdapter::load(fixtureID(fixtureMode), owned->fieldGeneration); owned->nextField = owned->field; owned->nextFieldGeneration = owned->fieldGeneration; } owned->fieldImage = makeVelocityFieldImage(owned->field); owned->fieldTexture = context.createTexture2D(); owned->nextFieldImage = makeVelocityFieldImage(owned->nextField); owned->nextFieldTexture = context.createTexture2D(); if (!owned->fieldImage || !owned->fieldTexture || !owned->nextFieldImage || !owned->nextFieldTexture) { Log::Error(Event::Render, "ParticleGPU: field resource creation failed"); return; } owned->fieldTexture->setImage(owned->fieldImage); owned->fieldTexture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Nearest, .wrapU = gfx::TextureWrapType::Repeat, .wrapV = gfx::TextureWrapType::Clamp}); owned->nextFieldTexture->setImage(owned->nextFieldImage); owned->nextFieldTexture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Nearest, .wrapU = gfx::TextureWrapType::Repeat, .wrapV = gfx::TextureWrapType::Clamp}); owned->gpuFieldGeneration = owned->fieldGeneration; owned->gpuNextFieldGeneration = owned->nextFieldGeneration; owned->fieldUploadCount = 2; owned->fixtureMode = fixtureMode; } catch (const std::exception& error) { Log::Error(Event::Render, "ParticleField: fixture load failed: " + std::string(error.what())); return; } gpuResources = std::move(owned); resources = gpuResources; Log::Info(Event::Render, "ParticleValidation30_02: State A/B create generation=" + std::to_string(resources->generation) + " size=16x16 independent=true"); Log::Info( Event::Render, "ParticleField: fixture loaded id=" + resources->field.fixtureID + " type=" + resources->field.fieldType + " bounds=[" + std::to_string(resources->field.west) + ',' + std::to_string(resources->field.east) + "]x[" + std::to_string(resources->field.south) + ',' + std::to_string(resources->field.north) + "] grid=" + std::to_string(resources->field.width) + 'x' + std::to_string(resources->field.height) + " dataVersion=" + std::to_string(resources->field.dataVersion) + " fieldGeneration=" + std::to_string(resources->fieldGeneration)); Log::Info(Event::Render, "ParticleField: fixture validated id=" + resources->field.fixtureID + " noData=" + std::to_string(resources->field.noData) + " velocityCount=" + std::to_string(resources->field.velocity.size())); Log::Info(Event::Render, "ParticleField: created id=" + resources->field.fixtureID + " generation=" + std::to_string(resources->fieldGeneration)); Log::Info(Event::Render, "ParticleGPU: field upload reason=RESOURCE_RECREATE fixture=" + resources->field.fixtureID + " fieldGeneration=" + std::to_string(resources->fieldGeneration) + " gpuFieldGeneration=" + std::to_string(resources->gpuFieldGeneration) + " resourceGeneration=" + std::to_string(resources->generation)); Log::Info(Event::Render, "ParticleGPU: time window T0=" + resources->field.fixtureID + "@" + std::to_string(resources->field.dataVersion) + " T1=" + resources->nextField.fixtureID + "@" + std::to_string(resources->nextField.dataVersion) + " uploads=" + std::to_string(resources->fieldUploadCount)); Log::Info( Event::Render, "ParticleValidation30_03: mask fixture ready id=coastal-mask-validation-v1 bounds=[-180,180]x[-85,85] " "encoding=uint8-land-ocean land=0 ocean=255 maskGeneration=" + std::to_string(resources->maskGeneration)); Log::Info(Event::Render, "ParticleValidation30_03: anchors A=0,0 B=0,-90 C=0,-172.8 D=0,172.8 canonical=Mercator-z0"); } const bool sourceChanged = formalSource && sourceField && sourceNextField && (particle::fieldIdentityChanged(resources->field, *sourceField) || particle::fieldIdentityChanged(resources->nextField, *sourceNextField)); if ((!formalSource && resources->fixtureMode != fixtureMode) || sourceChanged) { try { const auto oldT0 = resources->field; const auto oldT1 = resources->nextField; auto nextT0 = formalSource ? *sourceField : particle::OfflineFixtureParticleAdapter::load(fixtureID(fixtureMode), ++nextFieldGeneration); auto nextT1 = formalSource ? *sourceNextField : nextT0; if (!nextT0.isTimeInterpolationCompatibleWith(nextT1)) { Log::Error(Event::Render, "ParticleGPU: rejected incompatible time-window fields"); return; } const bool reuseOldT0 = particle::sameFieldGpuContent(oldT0, nextT0); const bool reuseOldT1 = particle::sameFieldGpuContent(oldT1, nextT0); const bool reuseNewT0 = particle::sameFieldGpuContent(oldT0, nextT1); const bool reuseNewT1 = particle::sameFieldGpuContent(oldT1, nextT1); const bool windowOverlap = reuseOldT0 || reuseOldT1 || reuseNewT0 || reuseNewT1; const auto upload = [&](const particle::ParticleFieldSnapshot& field, std::shared_ptr& image, gfx::Texture2DPtr& texture) { image = makeVelocityFieldImage(field); texture = context.createTexture2D(); if (!image || !texture) return false; texture->setImage(image); texture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Nearest, .wrapU = gfx::TextureWrapType::Repeat, .wrapV = gfx::TextureWrapType::Clamp}); ++resources->fieldUploadCount; return true; }; auto takeSlot = [&](const particle::ParticleFieldSnapshot& wanted, particle::ParticleFieldSnapshot& assigned, std::shared_ptr& image, gfx::Texture2DPtr& texture) { if (particle::sameFieldGpuContent(oldT0, wanted)) { assigned = oldT0; image = resources->fieldImage; texture = resources->fieldTexture; return true; } if (particle::sameFieldGpuContent(oldT1, wanted)) { assigned = oldT1; image = resources->nextFieldImage; texture = resources->nextFieldTexture; return true; } assigned = wanted; return upload(assigned, image, texture); }; particle::ParticleFieldSnapshot assignedT0; particle::ParticleFieldSnapshot assignedT1; std::shared_ptr imageT0; std::shared_ptr imageT1; gfx::Texture2DPtr textureT0; gfx::Texture2DPtr textureT1; if (!takeSlot(nextT0, assignedT0, imageT0, textureT0) || !takeSlot(nextT1, assignedT1, imageT1, textureT1)) { Log::Error(Event::Render, "ParticleGPU: field switch resource creation failed"); return; } resources->field = std::move(assignedT0); resources->nextField = std::move(assignedT1); resources->fieldImage = std::move(imageT0); resources->nextFieldImage = std::move(imageT1); resources->fieldTexture = std::move(textureT0); resources->nextFieldTexture = std::move(textureT1); resources->fieldGeneration = resources->field.generation; resources->nextFieldGeneration = resources->nextField.generation; resources->gpuFieldGeneration = resources->fieldGeneration; resources->gpuNextFieldGeneration = resources->nextFieldGeneration; resources->fixtureMode = fixtureMode; if (!windowOverlap) resources->resetSequence(); Log::Info(Event::Render, "ParticleGPU: time window advance T0=" + oldT0.fixtureID + " T1=" + oldT1.fixtureID + " -> T0=" + resources->field.fixtureID + " T1=" + resources->nextField.fixtureID + " reuse=" + (windowOverlap ? "true" : "false") + " uploads=" + std::to_string(resources->fieldUploadCount) + " simulationRebuild=" + (windowOverlap ? "false" : "true") + " trailCleared=" + (windowOverlap ? "false" : "true")); } catch (const std::exception& error) { Log::Error(Event::Render, "ParticleField: fixture switch failed: " + std::string(error.what())); return; } } const auto requestedTimeMix = std::clamp(properties.timeMix, 0.0f, 1.0f); if (resources->timeMix != requestedTimeMix) { resources->timeMix = requestedTimeMix; Log::Info(Event::Render, "ParticleGPU: timeMix=" + std::to_string(resources->timeMix) + " T0=" + resources->field.fixtureID + " T1=" + resources->nextField.fixtureID + " uploads=" + std::to_string(resources->fieldUploadCount) + " simulationRebuild=0 trailRebuild=0"); } if (!resources->lifecycleModeKnown || resources->lifecycleActive != active) { Log::Info(Event::Render, "ParticleRender: lifecycle transition=" + std::string(active ? "ACTIVE" : "STATIC") + " resourceGeneration=" + std::to_string(resources->generation) + " fixture=" + resources->field.fixtureID + " simulationFrame=" + std::to_string(resources->committedFrame) + " stateRead=" + slotName(resources->stateRead) + " trailRead=" + slotName(resources->trailRead)); resources->lifecycleModeKnown = true; resources->lifecycleActive = active; } if (resources->loggedMaskMode != maskMode) { Log::Info(Event::Render, "ParticleValidation30_03: mask mode=" + std::to_string(maskMode) + " consumer=" + (maskMode == 0 ? std::string("GLOBAL") : (maskMode == 1 ? "OCEAN_ONLY_A" : "OCEAN_ONLY_B")) + " maskGeneration=" + std::to_string(resources->maskGeneration) + " reuse=true"); resources->loggedMaskMode = maskMode; } removeTargets(resources->registeredTargets, changes); resources->registeredTargets.clear(); const auto viewport = validViewportSize(transformState.getSize()); const auto ensureTrailTargets = [&] { if (resources->trailTargets[0] && resources->trailTargets[1] && resources->trailSize == viewport) { return true; } const auto previousSize = resources->trailSize; resources->trailTargets = {}; for (uint8_t i = 0; i < 2; ++i) { resources->trailTargets[i] = createValidationTarget( context, viewport, getID() + "-trail-" + slotName(i), gfx::TextureFilterType::Linear); } if (!resources->trailTargets[0] || !resources->trailTargets[1]) { Log::Error(Event::Render, "ParticleGPU: Trail resource creation failed"); return false; } resources->trailSize = viewport; resources->trailRead = 0; resources->trailSeeded = false; Log::Info(Event::Render, "ParticleGPU: Trail A/B " + std::string(previousSize.isEmpty() ? "create" : "resize") + " resourceGeneration=" + std::to_string(resources->generation) + " old=" + std::to_string(previousSize.width) + "x" + std::to_string(previousSize.height) + " new=" + std::to_string(viewport.width) + "x" + std::to_string(viewport.height) + " StatePreserved=true"); return true; }; if (!active) { // STATIC freezes simulation only. A recreated activity still needs the // resource bootstrap and first current-frame composite without a button tap. if (!ensureTrailTargets()) { static_cast(*layerGroup).clearDrawables(); return; } resources->pending = false; auto& compositeGroup = static_cast(*layerGroup); if (!resources->stateSeeded || !resources->trailSeeded) { const auto stateWrite = resources->stateWrite(); const auto trailWrite = resources->trailWrite(); auto& stateGroup = static_cast(*resources->stateTargets[stateWrite]->getLayerGroup(0)); auto& trailGroup = static_cast(*resources->trailTargets[trailWrite]->getLayerGroup(0)); const bool stateBuilt = buildQuad(context, resources, stateGroup, getID() + "-static-bootstrap-state-pass", resources->stateTargets[resources->stateRead]->getTexture(), resources->stateTargets[stateWrite]->getTexture(), resources->maskTexture, {}, 0.0f, ValidationPass::State, resources->stateRead, stateWrite, 0, maskMode, true); const bool trailBuilt = buildQuad(context, resources, trailGroup, getID() + "-static-bootstrap-trail-pass", resources->trailTargets[resources->trailRead]->getTexture(), resources->trailTargets[trailWrite]->getTexture(), resources->maskTexture, resources->stateTargets[stateWrite]->getTexture(), 0.0f, ValidationPass::Trail, resources->trailRead, trailWrite, 0, maskMode, true); compositeGroup.clearDrawables(); bool compositeBuilt = true; const auto center = transformState.getLatLng(LatLng::Unwrapped); const auto centerWrap = wrapForLongitude(center.longitude()); for (int16_t wrap = static_cast(centerWrap - 1); wrap <= centerWrap + 1; ++wrap) { compositeBuilt = buildQuad(context, resources, compositeGroup, getID() + "-static-bootstrap-composite-pass-" + std::to_string(wrap), resources->trailTargets[trailWrite]->getTexture(), {}, resources->maskTexture, {}, 0.0f, ValidationPass::Composite, resources->trailRead, trailWrite, wrap, maskMode, false) && compositeBuilt; } if (!stateBuilt || !trailBuilt || !compositeBuilt) { compositeGroup.clearDrawables(); return; } resources->registeredTargets.emplace_back(resources->stateTargets[resources->stateRead]); resources->registeredTargets.emplace_back(resources->stateTargets[stateWrite]); resources->registeredTargets.emplace_back(resources->trailTargets[resources->trailRead]); resources->registeredTargets.emplace_back(resources->trailTargets[trailWrite]); for (const auto& target : resources->registeredTargets) { changes.emplace_back(std::make_unique(target)); } resources->bootstrapPending = true; resources->pending = true; Log::Info(Event::Render, "ParticleRender: STATIC bootstrap scheduled resourceGeneration=" + std::to_string(resources->generation) + " fixture=" + resources->field.fixtureID + " simulationFrame=" + std::to_string(resources->committedFrame)); return; } if (resources->trailTargets[resources->trailRead]) { compositeGroup.clearDrawables(); const auto center = transformState.getLatLng(LatLng::Unwrapped); const auto centerWrap = wrapForLongitude(center.longitude()); for (int16_t wrap = static_cast(centerWrap - 1); wrap <= centerWrap + 1; ++wrap) { buildQuad(context, resources, compositeGroup, getID() + "-frozen-composite-pass-" + std::to_string(wrap), resources->trailTargets[resources->trailRead]->getTexture(), {}, resources->maskTexture, {}, 0.0f, ValidationPass::FrozenComposite, resources->trailRead, resources->trailRead, wrap, maskMode, false); } } else { compositeGroup.clearDrawables(); } return; } resources->staticFreezeLogged = false; if (!ensureTrailTargets()) { return; } const auto stateWrite = resources->stateWrite(); const auto trailWrite = resources->trailWrite(); auto& stateGroup = static_cast(*resources->stateTargets[stateWrite]->getLayerGroup(0)); auto& trailGroup = static_cast(*resources->trailTargets[trailWrite]->getLayerGroup(0)); auto& compositeGroup = static_cast(*layerGroup); const bool stateBuilt = buildQuad(context, resources, stateGroup, getID() + "-state-pass", resources->stateTargets[resources->stateRead]->getTexture(), resources->stateTargets[stateWrite]->getTexture(), resources->maskTexture, {}, 1.0f, ValidationPass::State, resources->stateRead, stateWrite, 0, maskMode, true); const bool trailBuilt = buildQuad(context, resources, trailGroup, getID() + "-trail-pass", resources->trailTargets[resources->trailRead]->getTexture(), resources->trailTargets[trailWrite]->getTexture(), resources->maskTexture, resources->stateTargets[stateWrite]->getTexture(), 1.0f, ValidationPass::Trail, resources->trailRead, trailWrite, 0, maskMode, true); compositeGroup.clearDrawables(); bool compositeBuilt = true; const auto center = transformState.getLatLng(LatLng::Unwrapped); const auto centerWrap = wrapForLongitude(center.longitude()); for (int16_t wrap = static_cast(centerWrap - 1); wrap <= centerWrap + 1; ++wrap) { compositeBuilt = buildQuad(context, resources, compositeGroup, getID() + "-composite-pass-" + std::to_string(wrap), resources->trailTargets[trailWrite]->getTexture(), {}, resources->maskTexture, {}, 0.0f, ValidationPass::Composite, resources->trailRead, trailWrite, wrap, maskMode, false) && compositeBuilt; } if (!stateBuilt || !trailBuilt || !compositeBuilt) { compositeGroup.clearDrawables(); return; } // Read targets are registered only for their one-time clear. Normal frames register // only the opposite write targets, in dependency order, so a sampled texture can // never be attached to the active render pass. if (!resources->stateSeeded) { resources->registeredTargets.emplace_back(resources->stateTargets[resources->stateRead]); } resources->registeredTargets.emplace_back(resources->stateTargets[stateWrite]); if (!resources->trailSeeded) { resources->registeredTargets.emplace_back(resources->trailTargets[resources->trailRead]); } resources->registeredTargets.emplace_back(resources->trailTargets[trailWrite]); for (const auto& target : resources->registeredTargets) { changes.emplace_back(std::make_unique(target)); } resources->pending = true; if (resources->committedFrame < 3 || (resources->committedFrame + 1) % 60 == 0) { Log::Info(Event::Render, "ParticleValidation30_02: schedule generation=" + std::to_string(resources->generation) + " frame=" + std::to_string(resources->committedFrame + 1) + " state=" + slotName(resources->stateRead) + "->" + slotName(stateWrite) + " trail=" + slotName(resources->trailRead) + "->" + slotName(trailWrite) + " trailStateSource=" + slotName(stateWrite) + " compositeTrailSource=" + slotName(trailWrite) + " displayMode=" + (resources->formalDisplay ? "FORMAL_PARTICLE" : "VALIDATION") + " targetsDistinct=true cpuStateCopy=false"); } } } // namespace mbgl