Files
MapLibreParticleLab/src/mbgl/renderer/layers/render_particle_validation_layer.cpp
OpenAI Codex 1343929474
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
fix: keep particle timeline playback ticking
2026-09-04 08:17:29 +08:00

1455 lines
78 KiB
C++

#include <mbgl/renderer/layers/render_raster_particle_layer.hpp>
#include <mbgl/gfx/color_mode.hpp>
#include <mbgl/gfx/context.hpp>
#include <mbgl/gfx/cull_face_mode.hpp>
#include <mbgl/gfx/drawable.hpp>
#include <mbgl/gfx/drawable_builder.hpp>
#include <mbgl/gfx/drawable_tweaker.hpp>
#include <mbgl/gfx/index_vector.hpp>
#include <mbgl/gfx/shader_registry.hpp>
#include <mbgl/gfx/texture2d.hpp>
#include <mbgl/map/transform_state.hpp>
#include <mbgl/particle/field/offline_fixture_particle_adapter.hpp>
#include <mbgl/renderer/buckets/heatmap_bucket.hpp>
#include <mbgl/renderer/change_request.hpp>
#include <mbgl/renderer/layer_group.hpp>
#include <mbgl/renderer/paint_parameters.hpp>
#include <mbgl/renderer/render_pass.hpp>
#include <mbgl/renderer/render_static_data.hpp>
#include <mbgl/renderer/render_target.hpp>
#include <mbgl/renderer/sources/render_raster_array_source.hpp>
#include <mbgl/shaders/particle_gpu_validation_ubo.hpp>
#include <mbgl/shaders/shader_defines.hpp>
#include <mbgl/shaders/shader_program_base.hpp>
#include <mbgl/style/types.hpp>
#include <mbgl/style/layers/raster_particle_layer_impl.hpp>
#include <mbgl/style/layers/particle_render_layer_properties.hpp>
#include <mbgl/style/layers/particle_validation_layer_impl.hpp>
#include <mbgl/tile/tile_id.hpp>
#include <mbgl/util/image.hpp>
#include <mbgl/util/logging.hpp>
#include <mbgl/util/mat4.hpp>
#include <algorithm>
#include <array>
#include <atomic>
#include <cmath>
#include <chrono>
#include <limits>
#include <sstream>
#include <vector>
namespace mbgl {
namespace {
constexpr auto ShaderName = "ParticleGpuValidationShader";
constexpr Size MaskSize{16, 16};
std::atomic_uint64_t nextGpuGeneration{0};
std::atomic_uint64_t nextMaskGeneration{0};
std::atomic_uint64_t nextFieldGeneration{0};
std::atomic_uint64_t nextRenderLayerInstanceId{0};
struct ParticleSettings {
bool active;
uint8_t maskMode;
uint8_t fixtureMode;
bool usesFormalSource;
float timeMix;
uint32_t particleCount;
float lineWidth;
float particleLength;
float particleOpacity;
float trailOpacity;
float trailFade;
bool speedColorEnabled;
};
ParticleSettings particleSettings(const Immutable<style::Layer::Impl>& value) {
if (value->getTypeInfo() == style::RasterParticleLayer::Impl::staticTypeInfo()) {
const auto& layer = static_cast<const style::RasterParticleLayer::Impl&>(*value);
return {layer.active,
layer.maskMode,
0,
true,
layer.timeMix,
layer.particleCount,
layer.lineWidth,
layer.particleLength,
layer.particleOpacity,
layer.trailOpacity,
layer.trailFade,
layer.speedColorEnabled};
}
assert(value->getTypeInfo() == style::ParticleValidationLayer::Impl::staticTypeInfo());
const auto& layer = static_cast<const style::ParticleValidationLayer::Impl&>(*value);
return {layer.active, layer.maskMode, layer.fixtureMode, false, 0.0f, 256, 2.0f, 0.018f, 1.0f, 1.0f, 0.965f, false};
}
enum class ValidationPass : uint8_t {
State = 0,
Trail = 1,
Composite = 2,
FrozenComposite = 3,
TrailParticles = 4,
};
const char* slotName(const uint8_t slot) {
return slot == 0 ? "A" : "B";
}
Size validViewportSize(const Size size) {
return {std::max<uint32_t>(1, size.width), std::max<uint32_t>(1, size.height)};
}
Size particleStateSize(const uint32_t particleCount) {
const auto side = static_cast<uint32_t>(std::sqrt(static_cast<float>(particleCount)));
return {side, side};
}
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<PremultipliedImage> makeVelocityFieldImage(const particle::ParticleFieldSnapshot& field) {
auto image = std::make_shared<PremultipliedImage>(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<size_t>(y) * field.width + x) * 2;
const auto pixel = (static_cast<size_t>(y) * field.width + x) * PremultipliedImage::channels;
image->data[pixel + 0] = static_cast<uint8_t>(std::clamp(field.velocity[cell] * 0.5f + 0.5f, 0.0f, 1.0f) *
255.0f);
image->data[pixel + 1] = static_cast<uint8_t>(
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<PremultipliedImage> makeCoastalMaskFixture() {
auto image = std::make_shared<PremultipliedImage>(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<int16_t>(std::floor((longitude + 180.0) / 360.0));
}
void removeTargets(const std::vector<RenderTargetPtr>& targets, UniqueChangeRequestVec& changes) {
for (const auto& target : targets) {
if (target) {
changes.emplace_back(std::make_unique<RemoveRenderTargetRequest>(target));
}
}
}
template <std::size_t N>
void removeTargets(const std::array<RenderTargetPtr, N>& targets, UniqueChangeRequestVec& changes) {
for (const auto& target : targets) {
if (target) {
changes.emplace_back(std::make_unique<RemoveRenderTargetRequest>(target));
}
}
}
} // namespace
class ParticleGpuResources {
public:
uint64_t renderLayerInstanceId = 0;
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;
std::chrono::steady_clock::time_point lastPlaybackHeartbeat{};
uint64_t maskGeneration = 0;
uint64_t fieldGeneration = 0;
uint64_t gpuFieldGeneration = 0;
uint64_t nextFieldGeneration = 0;
uint64_t gpuNextFieldGeneration = 0;
uint64_t fieldUploadCount = 0;
uint64_t resetCount = 0;
float timeMix = 0.0f;
uint32_t particleCount = 0;
float lineWidth = 2.0f;
float particleLength = 0.018f;
float particleOpacity = 1.0f;
float trailOpacity = 1.0f;
float trailFade = 0.92f;
bool speedColorEnabled = true;
uint8_t fixtureMode = 255;
uint8_t loggedMaskMode = 255;
Size trailSize{};
Size stateSize{};
std::array<RenderTargetPtr, 2> stateTargets;
std::array<RenderTargetPtr, 2> trailTargets;
std::vector<RenderTargetPtr> 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<style::RasterArrayTimelineRuntime> timeline;
std::shared_ptr<PremultipliedImage> fieldImage;
std::shared_ptr<PremultipliedImage> nextFieldImage;
std::shared_ptr<PremultipliedImage> maskFixture;
std::shared_ptr<gfx::VertexVector<HeatmapTextureLayoutVertex>> vertices;
std::shared_ptr<gfx::VertexVector<HeatmapTextureLayoutVertex>> particleVertices;
gfx::IndexVectorBasePtr particleIndices;
SegmentVector segments;
SegmentVector particleSegments;
uint8_t stateWrite() const { return static_cast<uint8_t>(1 - stateRead); }
uint8_t trailWrite() const { return static_cast<uint8_t>(1 - trailRead); }
void resetSequence(const char* reason) {
++resetCount;
Log::Info(Event::Render,
"ParticleGPU: event=SIMULATION_RESET reason=" + std::string(reason) + " renderLayerInstanceId=" +
std::to_string(renderLayerInstanceId) + " resourceGeneration=" + std::to_string(generation) +
" resetCount=" + std::to_string(resetCount));
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 {
constexpr uint32_t MaxParticleVerticesPerSegment = std::numeric_limits<uint16_t>::max() + 1u;
void rebuildParticleDrawResources(const std::shared_ptr<ParticleGpuResources>& resources) {
auto vertices = std::make_shared<gfx::VertexVector<HeatmapTextureLayoutVertex>>();
// Each State texel is represented by a two-vertex line. Its endpoints are
// static texel indices; State position, interpolated Field velocity and
// orientation remain GPU-owned in the vertex shader.
vertices->reserve(resources->particleCount * 2);
for (uint32_t particleIndex = 0; particleIndex < resources->particleCount; ++particleIndex) {
const auto x = static_cast<int16_t>(particleIndex % resources->stateSize.width);
const auto y = static_cast<int16_t>(particleIndex / resources->stateSize.width);
// a_pos.x packs state X and endpoint (0=start, 1=end). State grids
// are at most 1024 wide, so the encoded value remains a signed short.
vertices->emplace_back(HeatmapBucket::textureVertex({static_cast<int16_t>(x * 2), y}));
vertices->emplace_back(HeatmapBucket::textureVertex({static_cast<int16_t>(x * 2 + 1), y}));
}
auto indices = std::make_shared<gfx::IndexVectorBase>();
indices->reserve(MaxParticleVerticesPerSegment);
for (uint32_t index = 0; index < MaxParticleVerticesPerSegment; ++index) {
indices->extend(1, static_cast<uint16_t>(index));
}
SegmentVector segments;
const auto vertexCount = resources->particleCount * 2;
for (uint32_t offset = 0; offset < vertexCount; offset += MaxParticleVerticesPerSegment) {
const auto count = std::min(vertexCount - offset, MaxParticleVerticesPerSegment);
segments.emplace_back(offset, 0, count, count);
}
resources->particleVertices = std::move(vertices);
resources->particleIndices = std::move(indices);
resources->particleSegments = std::move(segments);
}
class ParticleGpuValidationTweaker final : public gfx::DrawableTweaker {
public:
ParticleGpuValidationTweaker(std::shared_ptr<ParticleGpuResources> 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;
}
// update() is not called for every drawable frame. State executes once
// per particle simulation frame, making it the appropriate existing
// Core render lifecycle for the Source-owned monotonic playback clock.
if (pass == ValidationPass::State && resources->timeline) {
const auto frame = resources->timeline->tick();
resources->timeMix = frame.mix;
const auto now = std::chrono::steady_clock::now();
if (now - resources->lastPlaybackHeartbeat >= std::chrono::seconds(1)) {
resources->lastPlaybackHeartbeat = now;
Log::Info(
Event::Render,
"ParticleTimeline: event=PLAYBACK_TICK state=" + std::to_string(static_cast<int>(frame.state)) +
" speed=" + std::to_string(resources->timeline->playbackSpeed()) +
" cursor=" + std::to_string(frame.t0Index) + " mix=" + std::to_string(frame.mix) +
" window=" + std::to_string(frame.t0Index) + "/" + std::to_string(frame.t1Index) +
" fieldUploadCount=" + std::to_string(resources->fieldUploadCount) +
" simulationResetCount=" + std::to_string(resources->resetCount));
}
}
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;
// 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<float>(pass), frame, maskMode == 0 ? 0.0f : 1.0f, static_cast<float>(maskMode)},
.marker = {markerX, resources->speedColorEnabled ? 1.0f : 0.0f, 0.035f, 0.965f},
.mask = {static_cast<float>(resources->stateSize.width),
static_cast<float>(resources->stateSize.height),
resources->formalDisplay ? 1.0f : 0.0f,
seedState ? 1.0f : 0.0f},
.visual = {resources->particleOpacity,
resources->particleLength,
resources->trailOpacity,
resources->formalDisplay ? resources->trailFade : 0.965f},
.matrix = util::cast<float>(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<uint32_t>(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<ParticleGpuResources> 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<ParticleGpuResources>& 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<ParticleGpuValidationTweaker>(
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;
}
bool buildParticlePoints(gfx::Context& context,
const std::shared_ptr<ParticleGpuResources>& resources,
LayerGroup& group,
const std::string& name,
const gfx::Texture2DPtr& stateTexture,
const gfx::Texture2DPtr& target,
const uint8_t stateSlot,
const uint8_t trailSlot) {
if (!resources->particleVertices || !resources->particleIndices || resources->particleSegments.empty() ||
!stateTexture || !target) {
Log::Error(Event::Render, "ParticleGPU: event=PARTICLE_DRAW_REJECTED reason=MISSING_DRAW_RESOURCES");
return false;
}
auto attributes = context.createVertexAttributeArray();
if (const auto& attribute = attributes->set(shaders::idParticleGpuValidationPosVertexAttribute)) {
attribute->setSharedRawData(resources->particleVertices,
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(gfx::ColorMode::unblended());
builder->setCullFaceMode(gfx::CullFaceMode::disabled());
builder->setVertexAttributes(std::move(attributes));
builder->setRawVertices({}, resources->particleVertices->elements(), gfx::AttributeDataType::Short2);
builder->setSegments(gfx::Lines(resources->lineWidth),
resources->particleIndices,
resources->particleSegments.data(),
resources->particleSegments.size());
builder->setTexture(stateTexture, shaders::idParticleGpuValidationPreviousTexture);
builder->setTexture(stateTexture, shaders::idParticleGpuValidationStateTexture);
builder->addTweaker(std::make_shared<ParticleGpuValidationTweaker>(resources,
ValidationPass::TrailParticles,
stateSlot,
trailSlot,
stateTexture,
target,
resources->maskTexture,
stateTexture,
0.0f,
0,
0));
builder->flush(context);
auto drawables = builder->clearDrawables();
if (drawables.size() != 1 || !drawables.front()) {
Log::Error(Event::Render, "ParticleGPU: event=PARTICLE_DRAW_REJECTED reason=DRAWABLE_FINALIZE_FAILED");
return false;
}
group.addDrawable(std::move(drawables.front()));
return true;
}
} // namespace
RenderRasterParticleLayer::RenderRasterParticleLayer(Immutable<style::Layer::Impl> impl_)
: RenderLayer([&] {
const auto settings = particleSettings(impl_);
return makeMutable<style::ParticleRenderLayerProperties>(std::move(impl_),
settings.active,
settings.maskMode,
settings.fixtureMode,
settings.timeMix,
settings.particleCount,
settings.lineWidth,
settings.particleLength,
settings.particleOpacity,
settings.trailOpacity,
settings.trailFade,
settings.speedColorEnabled);
}()) {
renderLayerInstanceId = ++nextRenderLayerInstanceId;
formalSource = baseImpl->getTypeInfo() == style::RasterParticleLayer::Impl::staticTypeInfo();
Log::Info(Event::Render,
"ParticleGPU: event=LAYER_CREATE layerId=" + getID() +
" renderLayerInstanceId=" + std::to_string(renderLayerInstanceId) +
" source=" + (formalSource ? baseImpl->source : std::string("validation-only")));
}
RenderRasterParticleLayer::~RenderRasterParticleLayer() {
if (gpuResources) {
Log::Info(Event::Render,
"ParticleGPU: event=LAYER_DESTROY layerId=" + getID() +
" renderLayerInstanceId=" + std::to_string(renderLayerInstanceId) +
" resourceGeneration=" + std::to_string(gpuResources->generation) +
" contextDestroyed=" + (gpuResources->contextDestroyed ? "true" : "false"));
}
Log::Info(Event::Render, "ParticleValidation: RenderLayer destroy id=" + getID());
}
void RenderRasterParticleLayer::evaluate(const PropertyEvaluationParameters&) {
Log::Info(Event::Render,
"ParticleGPU: event=LAYER_CHANGED layerId=" + getID() +
" renderLayerInstanceId=" + std::to_string(renderLayerInstanceId));
const auto settings = particleSettings(baseImpl);
auto properties = makeMutable<style::ParticleRenderLayerProperties>(baseImpl,
settings.active,
settings.maskMode,
settings.fixtureMode,
settings.timeMix,
settings.particleCount,
settings.lineWidth,
settings.particleLength,
settings.particleOpacity,
settings.trailOpacity,
settings.trailFade,
settings.speedColorEnabled);
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) +
" particleCount=" + std::to_string(settings.particleCount) + " lineWidth=" +
std::to_string(settings.lineWidth) + " streakLength=" + std::to_string(settings.particleLength) +
" particleOpacity=" + std::to_string(settings.particleOpacity) + " trailOpacity=" +
std::to_string(settings.trailOpacity) + " trailFade=" + std::to_string(settings.trailFade) +
" speedColor=" + (settings.speedColorEnabled ? "true" : "false") + " stage=40-05");
}
bool RenderRasterParticleLayer::needsRepaint() const {
const auto& properties = static_cast<const style::ParticleRenderLayerProperties&>(*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) {
Log::Info(Event::Render,
"ParticleGPU: event=PARTICLE_RESOURCES_RELEASE reason=LAYER_REMOVED particleCount=" +
std::to_string(gpuResources->particleCount) +
" resourceGeneration=" + std::to_string(gpuResources->generation));
Log::Info(Event::Render,
"ParticleGPU: event=PARTICLE_DRAW_RESOURCES_RELEASE reason=LAYER_REMOVED streakVertexCount=" +
std::to_string(gpuResources->particleVertices ? gpuResources->particleVertices->elements() : 0) +
" fullParticleDraw=true resourceGeneration=" + std::to_string(gpuResources->generation));
removeTargets(gpuResources->registeredTargets, changes);
gpuResources->registeredTargets.clear();
}
RenderLayer::layerRemoved(changes);
}
void RenderRasterParticleLayer::markContextDestroyed() {
if (gpuResources) {
gpuResources->contextDestroyed = true;
Log::Info(Event::Render,
"ParticleGPU: event=PARTICLE_RESOURCES_RELEASE reason=CONTEXT_DESTROYED particleCount=" +
std::to_string(gpuResources->particleCount) +
" resourceGeneration=" + std::to_string(gpuResources->generation));
Log::Info(Event::Render,
"ParticleGPU: event=PARTICLE_DRAW_RESOURCES_RELEASE reason=CONTEXT_DESTROYED streakVertexCount=" +
std::to_string(gpuResources->particleVertices ? gpuResources->particleVertices->elements() : 0) +
" fullParticleDraw=true resourceGeneration=" + std::to_string(gpuResources->generation));
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<const RenderRasterArraySource&>(*parameters.source);
sourceTimeline = source.impl().timeline;
if (sourceTimeline) {
const auto frame = sourceTimeline->frame();
sourceField = *frame.t0;
sourceNextField = *frame.t1;
sourceTimelineWindowRevision = frame.windowRevision;
return;
}
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<UpdateParameters>&,
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<const style::ParticleRenderLayerProperties&>(*evaluatedProperties);
const bool active = properties.active;
const uint8_t maskMode = properties.maskMode;
const uint8_t fixtureMode = properties.fixtureMode;
const uint32_t particleCount = properties.particleCount;
float timelineMix = properties.timeMix;
if (formalSource && sourceTimeline) {
const auto frame = sourceTimeline->tick();
timelineMix = frame.mix;
if (frame.windowRevision != sourceTimelineWindowRevision) {
sourceField = *frame.t0;
sourceNextField = *frame.t1;
sourceTimelineWindowRevision = frame.windowRevision;
}
}
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<ParticleGpuResources>();
owned->renderLayerInstanceId = renderLayerInstanceId;
owned->generation = ++nextGpuGeneration;
owned->formalDisplay = formalSource;
owned->shader = context.getGenericShader(shaders, ShaderName);
owned->vertices = std::make_shared<gfx::VertexVector<HeatmapTextureLayoutVertex>>(
RenderStaticData::heatmapTextureVertices());
owned->segments = RenderStaticData::heatmapTextureSegments();
owned->particleCount = particleCount;
owned->stateSize = particleStateSize(owned->particleCount);
rebuildParticleDrawResources(owned);
for (uint8_t i = 0; i < 2; ++i) {
owned->stateTargets[i] = createValidationTarget(
context, owned->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);
Log::Info(Event::Render,
"ParticleGPU: event=FIELD_UPLOAD slot=T0 reason=RESOURCE_CREATE fieldUploadCount=1 "
"sourceGeneration=" +
std::to_string(owned->field.generation) +
" renderLayerInstanceId=" + std::to_string(owned->renderLayerInstanceId) +
" resourceGeneration=" + std::to_string(owned->generation));
owned->fieldTexture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Nearest,
.wrapU = gfx::TextureWrapType::Repeat,
.wrapV = gfx::TextureWrapType::Clamp});
owned->nextFieldTexture->setImage(owned->nextFieldImage);
Log::Info(Event::Render,
"ParticleGPU: event=FIELD_UPLOAD slot=T1 reason=RESOURCE_CREATE fieldUploadCount=2 "
"sourceGeneration=" +
std::to_string(owned->nextField.generation) +
" renderLayerInstanceId=" + std::to_string(owned->renderLayerInstanceId) +
" resourceGeneration=" + std::to_string(owned->generation));
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=" + std::to_string(resources->stateSize.width) + "x" +
std::to_string(resources->stateSize.height) + " independent=true");
Log::Info(Event::Render,
"ParticleGPU: event=PARTICLE_RESOURCES_CREATE reason=RESOURCE_CREATE requestedParticleCount=" +
std::to_string(particleCount) +
" effectiveParticleCount=" + std::to_string(resources->particleCount) + " stateGrid=" +
std::to_string(resources->stateSize.width) + "x" + std::to_string(resources->stateSize.height) +
" resourceGeneration=" + std::to_string(resources->generation));
Log::Info(Event::Render,
"ParticleGPU: event=PARTICLE_DRAW_RESOURCES_CREATE effectiveParticleCount=" +
std::to_string(resources->particleCount) + " stateGrid=" +
std::to_string(resources->stateSize.width) + "x" + std::to_string(resources->stateSize.height) +
" particleDrawCount=" + std::to_string(resources->particleCount) +
" streakVertexCount=" + std::to_string(resources->particleVertices->elements()) +
" fullParticleDraw=true resourceGeneration=" + std::to_string(resources->generation));
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");
}
if (resources->particleCount != particleCount) {
const auto previousCount = resources->particleCount;
const auto previousSize = resources->stateSize;
const auto requestedSize = particleStateSize(particleCount);
std::array<RenderTargetPtr, 2> replacementTargets;
for (uint8_t i = 0; i < 2; ++i) {
replacementTargets[i] = createValidationTarget(
context, requestedSize, getID() + "-state-" + slotName(i), gfx::TextureFilterType::Nearest);
}
if (!replacementTargets[0] || !replacementTargets[1]) {
Log::Error(Event::Render,
"ParticleGPU: event=PARTICLE_COUNT_REJECTED reason=STATE_RESOURCE_CREATE_FAILED "
"requestedParticleCount=" +
std::to_string(particleCount) +
" effectiveParticleCount=" + std::to_string(resources->particleCount) +
" resourceGeneration=" + std::to_string(resources->generation));
return;
}
Log::Info(Event::Render,
"ParticleGPU: event=PARTICLE_RESOURCES_RELEASE reason=PARTICLE_COUNT_CHANGE particleCount=" +
std::to_string(previousCount) + " stateGrid=" + std::to_string(previousSize.width) + "x" +
std::to_string(previousSize.height) +
" resourceGeneration=" + std::to_string(resources->generation));
Log::Info(Event::Render,
"ParticleGPU: event=PARTICLE_DRAW_RESOURCES_RELEASE reason=PARTICLE_COUNT_CHANGE streakVertexCount=" +
std::to_string(resources->particleVertices ? resources->particleVertices->elements() : 0) +
" fullParticleDraw=true resourceGeneration=" + std::to_string(resources->generation));
removeTargets(resources->stateTargets, changes);
resources->stateTargets = std::move(replacementTargets);
resources->particleCount = particleCount;
resources->stateSize = requestedSize;
rebuildParticleDrawResources(resources);
resources->resetSequence("PARTICLE_COUNT_CHANGE");
Log::Info(Event::Render,
"ParticleGPU: event=PARTICLE_RESOURCES_REBUILD reason=PARTICLE_COUNT_CHANGE requestedParticleCount=" +
std::to_string(particleCount) +
" effectiveParticleCount=" + std::to_string(resources->particleCount) + " stateGrid=" +
std::to_string(resources->stateSize.width) + "x" + std::to_string(resources->stateSize.height) +
" resourceGeneration=" + std::to_string(resources->generation));
Log::Info(Event::Render,
"ParticleGPU: event=PARTICLE_DRAW_RESOURCES_REBUILD effectiveParticleCount=" +
std::to_string(resources->particleCount) + " stateGrid=" +
std::to_string(resources->stateSize.width) + "x" + std::to_string(resources->stateSize.height) +
" particleDrawCount=" + std::to_string(resources->particleCount) +
" streakVertexCount=" + std::to_string(resources->particleVertices->elements()) +
" fullParticleDraw=true resourceGeneration=" + std::to_string(resources->generation));
}
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<PremultipliedImage>& 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;
Log::Info(Event::Render,
"ParticleGPU: event=FIELD_UPLOAD reason=NEW_CONTENT fieldUploadCount=" +
std::to_string(resources->fieldUploadCount) +
" sourceGeneration=" + std::to_string(field.generation) +
" renderLayerInstanceId=" + std::to_string(resources->renderLayerInstanceId) +
" resourceGeneration=" + std::to_string(resources->generation));
return true;
};
auto takeSlot = [&](const particle::ParticleFieldSnapshot& wanted,
const char* destination,
particle::ParticleFieldSnapshot& assigned,
std::shared_ptr<PremultipliedImage>& image,
gfx::Texture2DPtr& texture) {
if (particle::sameFieldGpuContent(oldT0, wanted)) {
assigned = wanted;
image = resources->fieldImage;
texture = resources->fieldTexture;
Log::Info(Event::Render,
"ParticleGPU: event=FIELD_REUSE from=OLD_T0 to=" + std::string(destination) +
" oldGeneration=" + std::to_string(oldT0.generation) +
" newGeneration=" + std::to_string(wanted.generation) +
" renderLayerInstanceId=" + std::to_string(resources->renderLayerInstanceId) +
" resourceGeneration=" + std::to_string(resources->generation));
return true;
}
if (particle::sameFieldGpuContent(oldT1, wanted)) {
assigned = wanted;
image = resources->nextFieldImage;
texture = resources->nextFieldTexture;
Log::Info(Event::Render,
"ParticleGPU: event=FIELD_REUSE from=OLD_T1 to=" + std::string(destination) +
" oldGeneration=" + std::to_string(oldT1.generation) +
" newGeneration=" + std::to_string(wanted.generation) +
" renderLayerInstanceId=" + std::to_string(resources->renderLayerInstanceId) +
" resourceGeneration=" + std::to_string(resources->generation));
return true;
}
assigned = wanted;
return upload(assigned, image, texture);
};
particle::ParticleFieldSnapshot assignedT0;
particle::ParticleFieldSnapshot assignedT1;
std::shared_ptr<PremultipliedImage> imageT0;
std::shared_ptr<PremultipliedImage> imageT1;
gfx::Texture2DPtr textureT0;
gfx::Texture2DPtr textureT1;
if (!takeSlot(nextT0, "NEW_T0", assignedT0, imageT0, textureT0) ||
!takeSlot(nextT1, "NEW_T1", 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("FIELD_NO_OVERLAP");
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;
}
}
// Keep the shared Core runtime alive in the per-frame drawable tweaker.
// This is a pointer to Source-owned state, not a second timeline.
resources->timeline = sourceTimeline;
const auto requestedTimeMix = std::clamp(timelineMix, 0.0f, 1.0f);
if (resources->timeMix != requestedTimeMix) {
resources->timeMix = requestedTimeMix;
if (!sourceTimeline || std::abs(resources->timeMix * 4.0f - std::round(resources->timeMix * 4.0f)) < 0.01f) {
Log::Info(Event::Render,
"ParticleGPU: event=TIME_MIX_UPDATE value=" + std::to_string(resources->timeMix) +
" T0=" + resources->field.fixtureID + " T1=" + resources->nextField.fixtureID +
" renderLayerInstanceId=" + std::to_string(resources->renderLayerInstanceId) +
" resourceGeneration=" + std::to_string(resources->generation) +
" fieldUploadCount=" + std::to_string(resources->fieldUploadCount) +
" resetCount=" + std::to_string(resources->resetCount));
}
}
const bool visualChanged = resources->lineWidth != properties.lineWidth ||
resources->particleLength != properties.particleLength ||
resources->particleOpacity != properties.particleOpacity ||
resources->trailOpacity != properties.trailOpacity ||
resources->trailFade != properties.trailFade ||
resources->speedColorEnabled != properties.speedColorEnabled;
if (visualChanged) {
resources->lineWidth = properties.lineWidth;
resources->particleLength = properties.particleLength;
resources->particleOpacity = properties.particleOpacity;
resources->trailOpacity = properties.trailOpacity;
resources->trailFade = properties.trailFade;
resources->speedColorEnabled = properties.speedColorEnabled;
Log::Info(Event::Render,
"ParticleGPU: event=VISUAL_PROPERTY_APPLIED lineWidth=" + std::to_string(resources->lineWidth) +
" streakLength=" + std::to_string(resources->particleLength) +
" particleOpacity=" + std::to_string(resources->particleOpacity) + " trailOpacity=" +
std::to_string(resources->trailOpacity) + " trailFade=" + std::to_string(resources->trailFade) +
" speedColor=" + (resources->speedColorEnabled ? "true" : "false") +
" renderLayerInstanceId=" + std::to_string(resources->renderLayerInstanceId) +
" resourceGeneration=" + std::to_string(resources->generation) +
" fieldUploadCount=" + std::to_string(resources->fieldUploadCount) +
" resetCount=" + std::to_string(resources->resetCount));
}
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&>(*layerGroup).clearDrawables();
return;
}
resources->pending = false;
auto& compositeGroup = static_cast<LayerGroup&>(*layerGroup);
if (!resources->stateSeeded || !resources->trailSeeded) {
const auto stateWrite = resources->stateWrite();
const auto trailWrite = resources->trailWrite();
auto& stateGroup = static_cast<LayerGroup&>(*resources->stateTargets[stateWrite]->getLayerGroup(0));
auto& trailGroup = static_cast<LayerGroup&>(*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);
const bool particlesBuilt = !resources->formalDisplay ||
buildParticlePoints(context,
resources,
trailGroup,
getID() + "-static-bootstrap-trail-particles",
resources->stateTargets[stateWrite]->getTexture(),
resources->trailTargets[trailWrite]->getTexture(),
stateWrite,
trailWrite);
compositeGroup.clearDrawables();
bool compositeBuilt = true;
const auto center = transformState.getLatLng(LatLng::Unwrapped);
const auto centerWrap = wrapForLongitude(center.longitude());
for (int16_t wrap = static_cast<int16_t>(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 || !particlesBuilt || !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<AddRenderTargetRequest>(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<int16_t>(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<LayerGroup&>(*resources->stateTargets[stateWrite]->getLayerGroup(0));
auto& trailGroup = static_cast<LayerGroup&>(*resources->trailTargets[trailWrite]->getLayerGroup(0));
auto& compositeGroup = static_cast<LayerGroup&>(*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);
const bool particlesBuilt = !resources->formalDisplay ||
buildParticlePoints(context,
resources,
trailGroup,
getID() + "-trail-particles",
resources->stateTargets[stateWrite]->getTexture(),
resources->trailTargets[trailWrite]->getTexture(),
stateWrite,
trailWrite);
compositeGroup.clearDrawables();
bool compositeBuilt = true;
const auto center = transformState.getLatLng(LatLng::Unwrapped);
const auto centerWrap = wrapForLongitude(center.longitude());
for (int16_t wrap = static_cast<int16_t>(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 || !particlesBuilt || !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<AddRenderTargetRequest>(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