1455 lines
78 KiB
C++
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
|