2137 lines
117 KiB
C++
2137 lines
117 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/particle/field/particle_gpu_math.hpp>
|
|
#include <mbgl/map/transform_state.hpp>
|
|
#include <mbgl/particle/field/offline_fixture_particle_adapter.hpp>
|
|
#include <mbgl/particle/renderer_debug_counters.hpp>
|
|
#include <mbgl/renderer/particle_viewport_reseed.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 <exception>
|
|
#include <limits>
|
|
#include <sstream>
|
|
#include <vector>
|
|
|
|
namespace mbgl {
|
|
|
|
namespace {
|
|
|
|
constexpr auto ShaderName = "ParticleGpuValidationShader";
|
|
constexpr Size MaskSize{16, 16};
|
|
// This is a validation visual clock, not a multiplier on canonical U/V. At
|
|
// 60 Hz it advances 20 simulation seconds per frame: a typical 5 m/s wind
|
|
// moves about 100 m (roughly 0.65 px at z9), while a 30 m/s sample remains
|
|
// below a four-pixel segment at that zoom.
|
|
constexpr float VisualSimulationSecondsPerRealSecond = 1200.0f;
|
|
constexpr float VisualSimulationReferenceZoom = 9.0f;
|
|
constexpr float MinVisualSimulationSecondsPerRealSecond = 300.0f;
|
|
constexpr float MaxVisualSimulationSecondsPerRealSecond = 153600.0f;
|
|
constexpr float MaxSimulationDeltaSeconds = 2400.0f;
|
|
constexpr float ParticleLifecycleRealSeconds = 12.0f;
|
|
constexpr float TrailFadeReferenceFramesPerSecond = 60.0f;
|
|
constexpr auto TrailCameraStableDelay = std::chrono::milliseconds(150);
|
|
constexpr float ViewportRespawnMargin = 0.15f;
|
|
constexpr float MinimumRespawnBoundsExtent = 0.000001f;
|
|
|
|
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;
|
|
float advectionScale;
|
|
bool speedColorEnabled;
|
|
std::array<float, 4> particleColor;
|
|
std::array<float, 4> trailColor;
|
|
};
|
|
|
|
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.advectionScale,
|
|
layer.speedColorEnabled,
|
|
layer.particleColor,
|
|
layer.trailColor};
|
|
}
|
|
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,
|
|
1.0f,
|
|
false,
|
|
{0.08f, 0.55f, 1.0f, 1.0f},
|
|
{1.0f, 1.0f, 1.0f, 1.0f}};
|
|
}
|
|
|
|
enum class ValidationPass : uint8_t {
|
|
State = 0,
|
|
Trail = 1,
|
|
Composite = 2,
|
|
FrozenComposite = 3,
|
|
TrailParticles = 4,
|
|
TrailParticleCoverage = 6,
|
|
};
|
|
|
|
const char* slotName(const uint8_t slot) {
|
|
return slot == 0 ? "A" : "B";
|
|
}
|
|
|
|
const char* textureDataTypeName(const gfx::TextureChannelDataType type) {
|
|
switch (type) {
|
|
case gfx::TextureChannelDataType::UnsignedByte:
|
|
return "UnsignedByte";
|
|
case gfx::TextureChannelDataType::HalfFloat:
|
|
return "HalfFloat";
|
|
case gfx::TextureChannelDataType::Float:
|
|
return "Float";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
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::ceil(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) {
|
|
float scale = 1.0f;
|
|
for (size_t index = 0; index < field.valid.size(); ++index) {
|
|
if (!field.valid[index]) continue;
|
|
scale = std::max(scale, std::abs(field.vector[index * 2]));
|
|
scale = std::max(scale, std::abs(field.vector[index * 2 + 1]));
|
|
}
|
|
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;
|
|
// RGBA8 UNORM has no exact midpoint. Byte 128 is an explicit
|
|
// signed zero; shaders round the sampled byte before decoding.
|
|
const bool valid = field.valid[y * field.width + x] != 0;
|
|
image->data[pixel + 0] = particle::encodeSignedVelocity8(field.vector[cell], scale, valid);
|
|
image->data[pixel + 1] = particle::encodeSignedVelocity8(field.vector[cell + 1], scale, valid);
|
|
image->data[pixel + 2] = 0;
|
|
image->data[pixel + 3] = field.valid[y * field.width + x] ? 255 : 0;
|
|
}
|
|
}
|
|
return image;
|
|
}
|
|
|
|
float fieldVelocityScale(const particle::ParticleFieldSnapshot& field) {
|
|
float scale = 1.0f;
|
|
for (size_t index = 0; index < field.valid.size(); ++index) {
|
|
if (!field.valid[index]) continue;
|
|
scale = std::max(scale, std::abs(field.vector[index * 2]));
|
|
scale = std::max(scale, std::abs(field.vector[index * 2 + 1]));
|
|
}
|
|
return scale;
|
|
}
|
|
|
|
double mercatorY(const double latitude) {
|
|
const auto radians = latitude * M_PI / 180.0;
|
|
return (1.0 - std::log(std::tan(radians) + 1.0 / std::cos(radians)) / M_PI) * 0.5;
|
|
}
|
|
|
|
std::array<float, 4> fieldBounds(const particle::ParticleFieldSnapshot& field) {
|
|
return {static_cast<float>((field.west + 180.0) / 360.0),
|
|
static_cast<float>(mercatorY(field.north)),
|
|
static_cast<float>((field.east + 180.0) / 360.0),
|
|
static_cast<float>(mercatorY(field.south))};
|
|
}
|
|
|
|
struct RespawnDomain {
|
|
std::array<float, 4> bounds;
|
|
bool fallback = true;
|
|
};
|
|
|
|
bool validMercatorBounds(const std::array<float, 4>& bounds) {
|
|
return std::isfinite(bounds[0]) && std::isfinite(bounds[1]) && std::isfinite(bounds[2]) &&
|
|
std::isfinite(bounds[3]) && bounds[2] - bounds[0] >= MinimumRespawnBoundsExtent &&
|
|
bounds[3] - bounds[1] >= MinimumRespawnBoundsExtent;
|
|
}
|
|
|
|
RespawnDomain viewportRespawnDomain(const TransformState& transformState,
|
|
const Size viewport,
|
|
const particle::ParticleFieldSnapshot& field) {
|
|
const auto fullFieldBounds = fieldBounds(field);
|
|
if (!validMercatorBounds(fullFieldBounds)) {
|
|
return {fullFieldBounds, true};
|
|
}
|
|
|
|
const std::array<ScreenCoordinate, 4> corners = {
|
|
ScreenCoordinate{0.0, 0.0},
|
|
ScreenCoordinate{static_cast<double>(viewport.width), 0.0},
|
|
ScreenCoordinate{0.0, static_cast<double>(viewport.height)},
|
|
ScreenCoordinate{static_cast<double>(viewport.width), static_cast<double>(viewport.height)},
|
|
};
|
|
float minX = std::numeric_limits<float>::infinity();
|
|
float minY = std::numeric_limits<float>::infinity();
|
|
float maxX = -std::numeric_limits<float>::infinity();
|
|
float maxY = -std::numeric_limits<float>::infinity();
|
|
for (const auto& corner : corners) {
|
|
const auto latLng = transformState.screenCoordinateToLatLng(corner, LatLng::Unwrapped);
|
|
const auto x = static_cast<float>((latLng.longitude() + 180.0) / 360.0);
|
|
const auto y = static_cast<float>(mercatorY(latLng.latitude()));
|
|
minX = std::min(minX, x);
|
|
minY = std::min(minY, y);
|
|
maxX = std::max(maxX, x);
|
|
maxY = std::max(maxY, y);
|
|
}
|
|
const auto width = maxX - minX;
|
|
const auto height = maxY - minY;
|
|
if (!std::isfinite(width) || !std::isfinite(height) || width < MinimumRespawnBoundsExtent ||
|
|
height < MinimumRespawnBoundsExtent) {
|
|
return {fullFieldBounds, true};
|
|
}
|
|
|
|
minX -= width * ViewportRespawnMargin;
|
|
maxX += width * ViewportRespawnMargin;
|
|
minY -= height * ViewportRespawnMargin;
|
|
maxY += height * ViewportRespawnMargin;
|
|
const std::array<float, 4> intersection = {
|
|
std::max(fullFieldBounds[0], minX),
|
|
std::max(fullFieldBounds[1], minY),
|
|
std::min(fullFieldBounds[2], maxX),
|
|
std::min(fullFieldBounds[3], maxY),
|
|
};
|
|
return validMercatorBounds(intersection) ? RespawnDomain{intersection, false}
|
|
: RespawnDomain{fullFieldBounds, true};
|
|
}
|
|
|
|
// 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;
|
|
bool trailCameraKnown = false;
|
|
bool trailCameraInvalidated = false;
|
|
bool viewportDensityActive = false;
|
|
bool viewportDensityLogged = false;
|
|
ParticleViewportReseedGesture viewportReseedGesture;
|
|
uint64_t lastCameraLogFrame = 0;
|
|
std::chrono::steady_clock::time_point lastPlaybackHeartbeat{};
|
|
std::chrono::steady_clock::time_point lastSimulationTick{};
|
|
std::chrono::steady_clock::time_point lastMotionLog{};
|
|
std::chrono::steady_clock::time_point lastLifecycleLog{};
|
|
std::chrono::steady_clock::time_point lastTrailCameraChange{};
|
|
uint64_t maskGeneration = 0;
|
|
uint64_t fieldGeneration = 0;
|
|
uint64_t gpuFieldGeneration = 0;
|
|
uint64_t nextFieldGeneration = 0;
|
|
uint64_t gpuNextFieldGeneration = 0;
|
|
float fieldScale = 1.0f;
|
|
float nextFieldScale = 1.0f;
|
|
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;
|
|
float advectionScale = 1.0f;
|
|
// Match the formal layer default so a newly allocated resource cannot emit
|
|
// one speed-colored frame before evaluated visual properties synchronize.
|
|
bool speedColorEnabled = false;
|
|
std::array<float, 4> particleColor{0.08f, 0.55f, 1.0f, 1.0f};
|
|
std::array<float, 4> trailColor{1.0f, 1.0f, 1.0f, 1.0f};
|
|
uint8_t fixtureMode = 255;
|
|
uint8_t loggedMaskMode = 255;
|
|
Size trailSize{};
|
|
Size stateSize{};
|
|
double trailCameraLatitude = 0.0;
|
|
double trailCameraLongitude = 0.0;
|
|
double trailCameraZoom = 0.0;
|
|
double trailCameraBearing = 0.0;
|
|
double trailCameraPitch = 0.0;
|
|
std::array<float, 4> respawnBounds{};
|
|
|
|
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;
|
|
std::shared_ptr<gfx::VertexVector<HeatmapTextureLayoutVertex>> particleCoverageVertices;
|
|
gfx::IndexVectorBasePtr particleIndices;
|
|
gfx::IndexVectorBasePtr particleCoverageIndices;
|
|
SegmentVector segments;
|
|
SegmentVector particleSegments;
|
|
SegmentVector particleCoverageSegments;
|
|
|
|
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;
|
|
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::GpuResourceRelease);
|
|
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;
|
|
viewportReseedGesture.cancel();
|
|
lastSimulationTick = {};
|
|
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) {
|
|
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::ParticleDrawableRebuild);
|
|
auto vertices = std::make_shared<gfx::VertexVector<HeatmapTextureLayoutVertex>>();
|
|
auto coverageVertices = std::make_shared<gfx::VertexVector<HeatmapTextureLayoutVertex>>();
|
|
vertices->reserve(resources->particleCount * 2);
|
|
coverageVertices->reserve(resources->particleCount * 4);
|
|
|
|
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);
|
|
// Formal line path: x*2 + endpoint (0=previous, 1=current).
|
|
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}));
|
|
// Metal coverage path: x*4 + corner (0..3). The shader projects the
|
|
// true current State position, then applies only a 0.5-pixel clip-space
|
|
// corner offset. Simulation/world coordinates are never modified.
|
|
for (int16_t corner = 0; corner < 4; ++corner) {
|
|
coverageVertices->emplace_back(
|
|
HeatmapBucket::textureVertex({static_cast<int16_t>(x * 4 + corner), 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));
|
|
}
|
|
|
|
constexpr uint32_t MaxCoverageVerticesPerSegment = MaxParticleVerticesPerSegment - (MaxParticleVerticesPerSegment % 4u);
|
|
constexpr uint32_t MaxCoverageParticlesPerSegment = MaxCoverageVerticesPerSegment / 4u;
|
|
auto coverageIndices = std::make_shared<gfx::IndexVectorBase>();
|
|
coverageIndices->reserve(MaxCoverageParticlesPerSegment * 6u);
|
|
for (uint32_t particle = 0; particle < MaxCoverageParticlesPerSegment; ++particle) {
|
|
const auto base = static_cast<uint16_t>(particle * 4u);
|
|
coverageIndices->extend(1, base + 0u);
|
|
coverageIndices->extend(1, base + 1u);
|
|
coverageIndices->extend(1, base + 2u);
|
|
coverageIndices->extend(1, base + 2u);
|
|
coverageIndices->extend(1, base + 1u);
|
|
coverageIndices->extend(1, base + 3u);
|
|
}
|
|
|
|
SegmentVector segments;
|
|
const auto lineVertexCount = resources->particleCount * 2u;
|
|
for (uint32_t offset = 0; offset < lineVertexCount; offset += MaxParticleVerticesPerSegment) {
|
|
const auto count = std::min(lineVertexCount - offset, MaxParticleVerticesPerSegment);
|
|
segments.emplace_back(offset, 0, count, count);
|
|
}
|
|
|
|
SegmentVector coverageSegments;
|
|
const auto coverageVertexCount = resources->particleCount * 4u;
|
|
for (uint32_t offset = 0; offset < coverageVertexCount; offset += MaxCoverageVerticesPerSegment) {
|
|
const auto vertexCount = std::min(coverageVertexCount - offset, MaxCoverageVerticesPerSegment);
|
|
const auto particleCount = vertexCount / 4u;
|
|
coverageSegments.emplace_back(offset, 0, vertexCount, particleCount * 6u);
|
|
}
|
|
|
|
resources->particleVertices = std::move(vertices);
|
|
resources->particleCoverageVertices = std::move(coverageVertices);
|
|
resources->particleIndices = std::move(indices);
|
|
resources->particleCoverageIndices = std::move(coverageIndices);
|
|
resources->particleSegments = std::move(segments);
|
|
resources->particleCoverageSegments = std::move(coverageSegments);
|
|
}
|
|
|
|
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_,
|
|
float wallDeltaSeconds_,
|
|
float effectiveVisualSpeedScale_,
|
|
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_),
|
|
wallDeltaSeconds(wallDeltaSeconds_),
|
|
effectiveVisualSpeedScale(effectiveVisualSpeedScale_),
|
|
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;
|
|
}
|
|
|
|
if (pass == ValidationPass::State) {
|
|
const auto now = std::chrono::steady_clock::now();
|
|
if (now - resources->lastMotionLog >= std::chrono::seconds(1)) {
|
|
resources->lastMotionLog = now;
|
|
const auto valid = std::find(resources->field.valid.begin(), resources->field.valid.end(), uint8_t{1});
|
|
if (valid != resources->field.valid.end()) {
|
|
const auto cell = static_cast<size_t>(std::distance(resources->field.valid.begin(), valid));
|
|
const auto row = cell / resources->field.width;
|
|
const auto latitude = resources->field.north +
|
|
(resources->field.south - resources->field.north) *
|
|
(static_cast<float>(row) / std::max(1u, resources->field.height - 1));
|
|
const auto u = resources->field.vector[cell * 2];
|
|
const auto v = resources->field.vector[cell * 2 + 1];
|
|
const auto delta = particle::mercatorVelocityDelta(u, v, latitude, simulationStep);
|
|
const auto dx = delta[0];
|
|
const auto dy = delta[1];
|
|
Log::Info(Event::Render,
|
|
"ParticleMotion: fieldId=" + resources->field.fieldID + " sampleU=" + std::to_string(u) +
|
|
" sampleV=" + std::to_string(v) +
|
|
" zoom=" + std::to_string(parameters.state.getZoom()) +
|
|
" referenceZoom=" + std::to_string(VisualSimulationReferenceZoom) +
|
|
" effectiveVisualSpeedScale=" + std::to_string(effectiveVisualSpeedScale) +
|
|
" wallDeltaSeconds=" + std::to_string(wallDeltaSeconds) + " simulationDeltaSeconds=" +
|
|
std::to_string(simulationStep) + " estimatedStateDeltaWorld=" + std::to_string(dx) +
|
|
"," + std::to_string(dy) + " estimatedPixelDelta=" +
|
|
std::to_string(std::sqrt(dx * dx + dy * dy) * 512.0f *
|
|
std::exp2(static_cast<float>(parameters.state.getZoom()))) +
|
|
" simulationFrame=" + std::to_string(resources->committedFrame));
|
|
}
|
|
}
|
|
if (resources->formalDisplay && now - resources->lastLifecycleLog >= std::chrono::seconds(1)) {
|
|
resources->lastLifecycleLog = now;
|
|
Log::Info(Event::Render,
|
|
"ParticleLifecycle: configuredSlots=" + std::to_string(resources->particleCount) +
|
|
" initialAge=HASH_STAGGERED maxAge=HASH_JITTERED_0.75_1.25 "
|
|
"lifecycleBaseRealSeconds=" +
|
|
std::to_string(ParticleLifecycleRealSeconds) +
|
|
" respawn=GPU_HASH_ON_INVALID_OR_AGE_EXPIRED"
|
|
" fieldId=" +
|
|
resources->field.fieldID +
|
|
" simulationFrame=" + std::to_string(resources->committedFrame) + " readback=disabled");
|
|
}
|
|
}
|
|
|
|
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,
|
|
resources->formalDisplay ? wallDeltaSeconds : static_cast<float>(maskMode)},
|
|
.marker = {markerX,
|
|
resources->speedColorEnabled ? 1.0f : 0.0f,
|
|
resources->formalDisplay ? ParticleLifecycleRealSeconds : 0.035f,
|
|
resources->formalDisplay ? TrailFadeReferenceFramesPerSecond : 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},
|
|
.fieldBounds = fieldBounds(resources->field),
|
|
.respawnBounds = resources->respawnBounds,
|
|
.fieldScale = {resources->fieldScale,
|
|
resources->nextFieldScale,
|
|
resources->viewportDensityActive ? 1.0f : 0.0f,
|
|
static_cast<float>(resources->trailSize.width)},
|
|
.advection = {resources->field.vectorSemantics == particle::VectorSemantics::Direction ? 1.0f : 0.0f,
|
|
resources->advectionScale,
|
|
0.00001f,
|
|
static_cast<float>(resources->trailSize.height)},
|
|
.particleColor = resources->particleColor,
|
|
.trailColor = resources->trailColor,
|
|
.weatherStops = {},
|
|
.weatherColors = {},
|
|
.weatherRamp = {},
|
|
.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 = resources->formalDisplay ? worldWrap == 0
|
|
: 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) + " field=" + resources->field.fieldID);
|
|
}
|
|
}
|
|
|
|
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;
|
|
float wallDeltaSeconds;
|
|
float effectiveVisualSpeedScale;
|
|
int16_t worldWrap;
|
|
uint8_t maskMode;
|
|
};
|
|
|
|
RenderTargetPtr createValidationTarget(gfx::Context& context,
|
|
const Size size,
|
|
const std::string& name,
|
|
const gfx::TextureFilterType filter,
|
|
const gfx::TextureChannelDataType type,
|
|
const char* targetKind) {
|
|
try {
|
|
auto target = context.createRenderTarget(size, type);
|
|
if (!target || !target->isRenderable() || !target->getTexture()) {
|
|
Log::Error(Event::Render,
|
|
"ParticleStateTarget: event=CREATE_FAILED kind=" + std::string(targetKind) +
|
|
" dataType=" + textureDataTypeName(type) + " name=" + name + " stage=validation");
|
|
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)) {
|
|
Log::Error(Event::Render,
|
|
"ParticleStateTarget: event=CREATE_FAILED kind=" + std::string(targetKind) +
|
|
" dataType=" + textureDataTypeName(type) + " name=" + name + " reason=LAYER_GROUP");
|
|
return {};
|
|
}
|
|
Log::Info(Event::Render,
|
|
"ParticleStateTarget: event=CREATE_SUCCESS kind=" + std::string(targetKind) +
|
|
" dataType=" + textureDataTypeName(type) + " name=" + name +
|
|
" size=" + std::to_string(size.width) + "x" + std::to_string(size.height));
|
|
return target;
|
|
} catch (const std::exception& error) {
|
|
Log::Error(Event::Render,
|
|
"ParticleStateTarget: event=CREATE_FAILED kind=" + std::string(targetKind) + " dataType=" +
|
|
textureDataTypeName(type) + " name=" + name + " stage=exception error=" + error.what());
|
|
return {};
|
|
} catch (...) {
|
|
Log::Error(Event::Render,
|
|
"ParticleStateTarget: event=CREATE_FAILED kind=" + std::string(targetKind) +
|
|
" dataType=" + textureDataTypeName(type) + " name=" + name + " stage=unknown_exception");
|
|
return {};
|
|
}
|
|
}
|
|
|
|
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,
|
|
const float wallDeltaSeconds = 0.0f,
|
|
const float effectiveVisualSpeedScale = VisualSimulationSecondsPerRealSecond) {
|
|
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,
|
|
wallDeltaSeconds,
|
|
effectiveVisualSpeedScale,
|
|
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& previousStateTexture,
|
|
const gfx::Texture2DPtr& currentStateTexture,
|
|
const gfx::Texture2DPtr& target,
|
|
const uint8_t stateSlot,
|
|
const uint8_t trailSlot) {
|
|
if (!resources->particleVertices || !resources->particleIndices || resources->particleSegments.empty() ||
|
|
!previousStateTexture || !currentStateTexture || !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(previousStateTexture, shaders::idParticleGpuValidationPreviousTexture);
|
|
builder->setTexture(currentStateTexture, shaders::idParticleGpuValidationStateTexture);
|
|
builder->addTweaker(std::make_shared<ParticleGpuValidationTweaker>(resources,
|
|
ValidationPass::TrailParticles,
|
|
stateSlot,
|
|
trailSlot,
|
|
previousStateTexture,
|
|
target,
|
|
resources->maskTexture,
|
|
currentStateTexture,
|
|
0.0f,
|
|
0.0f,
|
|
VisualSimulationSecondsPerRealSecond,
|
|
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;
|
|
}
|
|
|
|
#if MLN_RENDER_BACKEND_METAL
|
|
bool buildParticleEndpointCoverage(gfx::Context& context,
|
|
const std::shared_ptr<ParticleGpuResources>& resources,
|
|
LayerGroup& group,
|
|
const std::string& name,
|
|
const gfx::Texture2DPtr& previousStateTexture,
|
|
const gfx::Texture2DPtr& currentStateTexture,
|
|
const gfx::Texture2DPtr& target,
|
|
const uint8_t stateSlot,
|
|
const uint8_t trailSlot) {
|
|
if (!resources->particleCoverageVertices || !resources->particleCoverageIndices ||
|
|
resources->particleCoverageSegments.empty() || !previousStateTexture || !currentStateTexture || !target) {
|
|
Log::Error(Event::Render, "ParticleGPU: event=PARTICLE_COVERAGE_REJECTED reason=MISSING_DRAW_RESOURCES");
|
|
return false;
|
|
}
|
|
|
|
auto attributes = context.createVertexAttributeArray();
|
|
if (const auto& attribute = attributes->set(shaders::idParticleGpuValidationPosVertexAttribute)) {
|
|
attribute->setSharedRawData(resources->particleCoverageVertices,
|
|
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->particleCoverageVertices->elements(), gfx::AttributeDataType::Short2);
|
|
builder->setSegments(gfx::Triangles(),
|
|
resources->particleCoverageIndices,
|
|
resources->particleCoverageSegments.data(),
|
|
resources->particleCoverageSegments.size());
|
|
builder->setTexture(previousStateTexture, shaders::idParticleGpuValidationPreviousTexture);
|
|
builder->setTexture(currentStateTexture, shaders::idParticleGpuValidationStateTexture);
|
|
builder->addTweaker(std::make_shared<ParticleGpuValidationTweaker>(resources,
|
|
ValidationPass::TrailParticleCoverage,
|
|
stateSlot,
|
|
trailSlot,
|
|
previousStateTexture,
|
|
target,
|
|
resources->maskTexture,
|
|
currentStateTexture,
|
|
0.0f,
|
|
0.0f,
|
|
VisualSimulationSecondsPerRealSecond,
|
|
0,
|
|
0));
|
|
builder->flush(context);
|
|
auto drawables = builder->clearDrawables();
|
|
if (drawables.size() != 1 || !drawables.front()) {
|
|
Log::Error(Event::Render, "ParticleGPU: event=PARTICLE_COVERAGE_REJECTED reason=DRAWABLE_FINALIZE_FAILED");
|
|
return false;
|
|
}
|
|
group.addDrawable(std::move(drawables.front()));
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
} // 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.advectionScale,
|
|
settings.speedColorEnabled,
|
|
settings.particleColor,
|
|
settings.trailColor);
|
|
}()) {
|
|
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.advectionScale,
|
|
settings.speedColorEnabled,
|
|
settings.particleColor,
|
|
settings.trailColor);
|
|
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->fieldID : 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) +
|
|
" advectionScale=" + std::to_string(settings.advectionScale) +
|
|
" speedColor=" + (settings.speedColorEnabled ? "true" : "false") + " stage=40-05");
|
|
}
|
|
|
|
bool RenderRasterParticleLayer::needsRepaint() const {
|
|
if (!isParticleAnimationActive()) return false;
|
|
const bool repaint = !formalSource ||
|
|
(sourceUsesV4Projection
|
|
? sourceV4Cadence && sourceV4Cadence->cadenceDue(std::chrono::steady_clock::now())
|
|
: !sourceTimeline || sourceTimeline->cadenceDue(std::chrono::steady_clock::now()));
|
|
if (repaint) particle::incrementRendererDebugCounter(particle::RendererDebugCounter::ParticleNeedsRepaint);
|
|
return repaint;
|
|
}
|
|
|
|
bool RenderRasterParticleLayer::isParticleAnimationActive() const {
|
|
const auto& properties = static_cast<const style::ParticleRenderLayerProperties&>(*evaluatedProperties);
|
|
if (!properties.active || (formalSource && (!sourceField || !sourceNextField)) ||
|
|
baseImpl->visibility == style::VisibilityType::None)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
bool RenderRasterParticleLayer::needsHiddenLayerSourceCleanup(const RenderSource& renderSource) const {
|
|
if (!formalSource || !gpuResources || renderSource.baseImpl->type != style::SourceType::RasterArray) return false;
|
|
const auto& source = static_cast<const RenderRasterArraySource&>(renderSource);
|
|
const auto& impl = source.impl();
|
|
return impl.hostFieldSubmission && !impl.field && !impl.nextField && !impl.timeline;
|
|
}
|
|
|
|
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);
|
|
sourceUsesV4Projection = false;
|
|
sourceProjectionMix = 0.0f;
|
|
if (source.impl().v4RendererState.particle) {
|
|
const auto& projection = *source.impl().v4RendererState.particle;
|
|
sourceField = projection.t0;
|
|
sourceNextField = projection.t1;
|
|
sourceProjectionMix = projection.sampleState.mix;
|
|
sourceTimeline.reset();
|
|
sourceV4Cadence = source.impl().v4Cadence;
|
|
sourceUsesV4Projection = true;
|
|
return;
|
|
}
|
|
sourceV4Cadence.reset();
|
|
sourceTimeline = source.impl().timeline;
|
|
if (sourceTimeline) {
|
|
const auto frame = sourceTimeline->frame();
|
|
sourceField = frame.t0;
|
|
sourceNextField = frame.t1;
|
|
sourceTimelineWindowRevision = frame.windowRevision;
|
|
return;
|
|
}
|
|
sourceField.reset();
|
|
sourceNextField.reset();
|
|
if (source.impl().field) {
|
|
sourceField = std::make_shared<const particle::ParticleFieldSnapshot>(*source.impl().field);
|
|
sourceNextField = source.impl().nextField
|
|
? std::make_shared<const particle::ParticleFieldSnapshot>(*source.impl().nextField)
|
|
: sourceField;
|
|
}
|
|
}
|
|
|
|
void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
|
gfx::Context& context,
|
|
const TransformState& transformState,
|
|
const std::shared_ptr<UpdateParameters>&,
|
|
const RenderTree&,
|
|
UniqueChangeRequestVec& changes) {
|
|
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::ParticleUpdate);
|
|
if (!layerGroup) {
|
|
if (auto group = context.createLayerGroup(layerIndex, 1, getID())) {
|
|
setLayerGroup(std::move(group), changes);
|
|
}
|
|
}
|
|
if (formalSource && (!sourceField || !sourceNextField)) {
|
|
if (layerGroup) static_cast<LayerGroup&>(*layerGroup).clearDrawables();
|
|
if (gpuResources) {
|
|
Log::Info(Event::Render,
|
|
"WeatherRuntime: event=PARTICLE_RESOURCE_RELEASE reason=FIELD_WINDOW_CLEAR layerId=" + getID());
|
|
Log::Info(Event::Render,
|
|
"WeatherRuntime: event=TRAIL_RESOURCE_RELEASE reason=FIELD_WINDOW_CLEAR layerId=" + getID());
|
|
removeTargets(gpuResources->registeredTargets, changes);
|
|
gpuResources->registeredTargets.clear();
|
|
// Targets contain tweakers retaining Resources; break that cycle.
|
|
for (const auto& target : gpuResources->stateTargets)
|
|
if (target && target->getLayerGroup(0)) target->getLayerGroup(0)->clearDrawables();
|
|
for (const auto& target : gpuResources->trailTargets)
|
|
if (target && target->getLayerGroup(0)) target->getLayerGroup(0)->clearDrawables();
|
|
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::GpuResourceRelease);
|
|
gpuResources.reset();
|
|
}
|
|
return;
|
|
}
|
|
// Particle advection requires a compatible vector pair. Scalar-only
|
|
// remains unavailable; VELOCITY and DIRECTION are resolved in the shader.
|
|
if (formalSource &&
|
|
(!sourceField->hasVector() || !sourceNextField->hasVector() || !sourceField->vectorSemantics ||
|
|
!sourceNextField->vectorSemantics || sourceField->vectorSemantics != sourceNextField->vectorSemantics)) {
|
|
if (layerGroup) static_cast<LayerGroup&>(*layerGroup).clearDrawables();
|
|
if (gpuResources) {
|
|
removeTargets(gpuResources->registeredTargets, changes);
|
|
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::GpuResourceRelease);
|
|
gpuResources.reset();
|
|
}
|
|
return;
|
|
}
|
|
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;
|
|
const float advectionScale = properties.advectionScale;
|
|
float timelineMix = sourceUsesV4Projection ? sourceProjectionMix : properties.timeMix;
|
|
bool cadenceAdvanced = true;
|
|
uint64_t cadenceRevision = sourceTimelineCadenceRevision;
|
|
if (formalSource && sourceTimeline) {
|
|
const auto advance = sourceTimeline->advanceIfDue(std::chrono::steady_clock::now());
|
|
const auto& frame = advance.frame;
|
|
timelineMix = frame.mix;
|
|
cadenceRevision = frame.cadenceRevision;
|
|
cadenceAdvanced = frame.cadenceRevision != sourceTimelineCadenceRevision;
|
|
if (frame.windowRevision != sourceTimelineWindowRevision) {
|
|
sourceField = frame.t0;
|
|
sourceNextField = frame.t1;
|
|
sourceTimelineWindowRevision = frame.windowRevision;
|
|
}
|
|
} else if (formalSource && sourceUsesV4Projection && sourceV4Cadence) {
|
|
cadenceAdvanced = sourceV4Cadence->consumeIfDue(std::chrono::steady_clock::now());
|
|
}
|
|
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.
|
|
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::GpuResourceRelease);
|
|
gpuResources.reset();
|
|
resources.reset();
|
|
}
|
|
if (!resources) {
|
|
auto owned = std::make_shared<ParticleGpuResources>();
|
|
owned->renderLayerInstanceId = renderLayerInstanceId;
|
|
owned->generation = ++nextGpuGeneration;
|
|
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::GpuResourceCreate);
|
|
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,
|
|
gfx::TextureChannelDataType::Float,
|
|
"STATE");
|
|
}
|
|
if (!owned->shader || !owned->stateTargets[0] || !owned->stateTargets[1]) {
|
|
Log::Error(
|
|
Event::Render,
|
|
"ParticleStateTarget: event=CREATE_FAILED kind=STATE dataType=Float reason=FRAMEBUFFER_UNAVAILABLE");
|
|
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::Clamp,
|
|
.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::Clamp,
|
|
.wrapV = gfx::TextureWrapType::Clamp});
|
|
owned->gpuFieldGeneration = owned->fieldGeneration;
|
|
owned->gpuNextFieldGeneration = owned->nextFieldGeneration;
|
|
owned->fieldUploadCount = 2;
|
|
owned->fieldScale = fieldVelocityScale(owned->field);
|
|
owned->nextFieldScale = fieldVelocityScale(owned->nextField);
|
|
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: field loaded id=" + resources->field.fieldID + " 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: field validated id=" + resources->field.fieldID +
|
|
" noData=" + std::to_string(resources->field.noData) +
|
|
" vectorCount=" + std::to_string(resources->field.vector.size()));
|
|
Log::Info(Event::Render,
|
|
"ParticleField: created id=" + resources->field.fieldID +
|
|
" generation=" + std::to_string(resources->fieldGeneration));
|
|
Log::Info(Event::Render,
|
|
"ParticleGPU: field upload reason=RESOURCE_RECREATE field=" + resources->field.fieldID +
|
|
" 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.fieldID + "@" +
|
|
std::to_string(resources->field.dataVersion) + " T1=" + resources->nextField.fieldID + "@" +
|
|
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");
|
|
}
|
|
|
|
// This is a renderer uniform only. It intentionally preserves State,
|
|
// Trail, Timeline, and uploaded field textures when changed.
|
|
resources->advectionScale = advectionScale;
|
|
|
|
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,
|
|
gfx::TextureChannelDataType::Float,
|
|
"STATE");
|
|
}
|
|
if (!replacementTargets[0] || !replacementTargets[1]) {
|
|
Log::Error(Event::Render,
|
|
"ParticleGPU: event=PARTICLE_COUNT_REJECTED reason=STATE_RESOURCE_CREATE_FAILED dataType=Float "
|
|
"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::sameFieldGpuEndpoint(resources->field, *sourceField) ||
|
|
!particle::sameFieldGpuEndpoint(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::sameFieldGpuEndpoint(oldT0, nextT0);
|
|
const bool reuseOldT1 = particle::sameFieldGpuEndpoint(oldT1, nextT0);
|
|
const bool reuseNewT0 = particle::sameFieldGpuEndpoint(oldT0, nextT1);
|
|
const bool reuseNewT1 = particle::sameFieldGpuEndpoint(oldT1, nextT1);
|
|
const bool windowOverlap = reuseOldT0 || reuseOldT1 || reuseNewT0 || reuseNewT1;
|
|
const auto upload = [&](const particle::ParticleFieldSnapshot& field,
|
|
std::shared_ptr<PremultipliedImage>& image,
|
|
gfx::Texture2DPtr& texture,
|
|
float& scale) {
|
|
image = makeVelocityFieldImage(field);
|
|
texture = context.createTexture2D();
|
|
if (!image || !texture) return false;
|
|
texture->setImage(image);
|
|
texture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Nearest,
|
|
.wrapU = gfx::TextureWrapType::Clamp,
|
|
.wrapV = gfx::TextureWrapType::Clamp});
|
|
scale = fieldVelocityScale(field);
|
|
++resources->fieldUploadCount;
|
|
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::FieldTextureUpload);
|
|
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,
|
|
float& scale) {
|
|
if (particle::gpuEndpointSource(oldT0, oldT1, wanted) == particle::GpuEndpointSource::OldT0 &&
|
|
resources->fieldTexture) {
|
|
assigned = wanted;
|
|
image = resources->fieldImage;
|
|
texture = resources->fieldTexture;
|
|
scale = resources->fieldScale;
|
|
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::gpuEndpointSource(oldT0, oldT1, wanted) == particle::GpuEndpointSource::OldT1 &&
|
|
resources->nextFieldTexture) {
|
|
assigned = wanted;
|
|
image = resources->nextFieldImage;
|
|
texture = resources->nextFieldTexture;
|
|
scale = resources->nextFieldScale;
|
|
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, scale);
|
|
};
|
|
particle::ParticleFieldSnapshot assignedT0;
|
|
particle::ParticleFieldSnapshot assignedT1;
|
|
std::shared_ptr<PremultipliedImage> imageT0;
|
|
std::shared_ptr<PremultipliedImage> imageT1;
|
|
gfx::Texture2DPtr textureT0;
|
|
gfx::Texture2DPtr textureT1;
|
|
float scaleT0 = 1.0f;
|
|
float scaleT1 = 1.0f;
|
|
if (!takeSlot(nextT0, "NEW_T0", assignedT0, imageT0, textureT0, scaleT0)) {
|
|
Log::Error(Event::Render, "ParticleGPU: field switch resource creation failed");
|
|
return;
|
|
}
|
|
if (particle::sameFieldGpuEndpoint(nextT0, nextT1)) {
|
|
assignedT1 = assignedT0;
|
|
imageT1 = imageT0;
|
|
textureT1 = textureT0;
|
|
scaleT1 = scaleT0;
|
|
} else if (!takeSlot(nextT1, "NEW_T1", assignedT1, imageT1, textureT1, scaleT1)) {
|
|
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->fieldScale = scaleT0;
|
|
resources->nextFieldScale = scaleT1;
|
|
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.fieldID + " T1=" + oldT1.fieldID +
|
|
" -> T0=" + resources->field.fieldID + " T1=" + resources->nextField.fieldID +
|
|
" 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.fieldID + " T1=" + resources->nextField.fieldID +
|
|
" 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 ||
|
|
resources->particleColor != properties.particleColor ||
|
|
resources->trailColor != properties.trailColor;
|
|
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;
|
|
resources->particleColor = properties.particleColor;
|
|
resources->trailColor = properties.trailColor;
|
|
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.fieldID + " 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 trailCamera = transformState.getLatLng(LatLng::Unwrapped);
|
|
const auto cameraNow = std::chrono::steady_clock::now();
|
|
const bool viewportChanged = resources->formalDisplay && resources->trailTargets[0] && resources->trailTargets[1] &&
|
|
resources->trailSize != viewport;
|
|
const bool transformChanged = resources->formalDisplay && resources->trailCameraKnown &&
|
|
(std::abs(resources->trailCameraLatitude - trailCamera.latitude()) > 0.00001 ||
|
|
std::abs(resources->trailCameraLongitude - trailCamera.longitude()) > 0.00001 ||
|
|
std::abs(resources->trailCameraZoom - transformState.getZoom()) > 0.0001 ||
|
|
std::abs(resources->trailCameraBearing - transformState.getBearing()) > 0.0001 ||
|
|
std::abs(resources->trailCameraPitch - transformState.getPitch()) > 0.0001);
|
|
bool cameraBecameStable = false;
|
|
if (viewportChanged) {
|
|
if (transformChanged) {
|
|
resources->viewportReseedGesture.begin(resources->trailCameraZoom);
|
|
}
|
|
const auto previousSize = resources->trailSize;
|
|
resources->trailTargets = {};
|
|
resources->trailSize = {};
|
|
resources->trailRead = 0;
|
|
resources->trailSeeded = false;
|
|
resources->trailCameraInvalidated = true;
|
|
resources->viewportDensityLogged = false;
|
|
resources->lastTrailCameraChange = cameraNow;
|
|
Log::Info(Event::Render,
|
|
"ParticleTrail: event=VIEWPORT_RESIZE 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 fieldResubmit=false timelineReset=false");
|
|
} else if (transformChanged) {
|
|
resources->viewportReseedGesture.begin(resources->trailCameraZoom);
|
|
if (!resources->trailCameraInvalidated) {
|
|
const auto reason = std::abs(resources->trailCameraZoom - transformState.getZoom()) > 0.0001 ? "zoom"
|
|
: std::abs(resources->trailCameraBearing - transformState.getBearing()) > 0.0001
|
|
? "rotate"
|
|
: std::abs(resources->trailCameraPitch - transformState.getPitch()) > 0.0001 ? "pitch"
|
|
: "pan";
|
|
resources->trailCameraInvalidated = true;
|
|
resources->trailSeeded = false;
|
|
resources->viewportDensityLogged = false;
|
|
Log::Info(Event::Render,
|
|
"ParticleTrail: event=CAMERA_INVALIDATE reason=" + std::string(reason) +
|
|
" statePreserved=true fieldResubmit=false timelineReset=false");
|
|
}
|
|
resources->lastTrailCameraChange = cameraNow;
|
|
} else if (resources->trailCameraInvalidated &&
|
|
cameraNow - resources->lastTrailCameraChange >= TrailCameraStableDelay) {
|
|
resources->trailCameraInvalidated = false;
|
|
cameraBecameStable = true;
|
|
}
|
|
resources->trailCameraKnown = true;
|
|
resources->trailCameraLatitude = trailCamera.latitude();
|
|
resources->trailCameraLongitude = trailCamera.longitude();
|
|
resources->trailCameraZoom = transformState.getZoom();
|
|
resources->trailCameraBearing = transformState.getBearing();
|
|
resources->trailCameraPitch = transformState.getPitch();
|
|
const auto respawnDomain = viewportRespawnDomain(transformState, viewport, resources->field);
|
|
resources->viewportDensityActive = resources->formalDisplay && !resources->trailCameraInvalidated &&
|
|
!respawnDomain.fallback;
|
|
// During a camera gesture, State continues normally but all ordinary
|
|
// lifecycle respawns stay in the full Field. The viewport domain is only
|
|
// enabled after the existing stable-camera debounce has completed.
|
|
resources->respawnBounds = resources->viewportDensityActive ? respawnDomain.bounds : fieldBounds(resources->field);
|
|
if (cameraBecameStable) {
|
|
const auto reseedDecision = resources->viewportReseedGesture.finish(transformState.getZoom(),
|
|
resources->viewportDensityActive);
|
|
if (reseedDecision.action == ParticleViewportReseedAction::Reseed) {
|
|
resources->stateSeeded = false;
|
|
resources->trailSeeded = false;
|
|
const auto& bounds = resources->respawnBounds;
|
|
Log::Info(Event::Render,
|
|
"ParticleDensity: event=VIEWPORT_RESEED reason=ZOOM_OUT startZoom=" +
|
|
std::to_string(reseedDecision.startZoom) + " endZoom=" +
|
|
std::to_string(reseedDecision.endZoom) + " respawnBounds=[" +
|
|
std::to_string(bounds[0]) + "," + std::to_string(bounds[1]) + "," +
|
|
std::to_string(bounds[2]) + "," + std::to_string(bounds[3]) +
|
|
"] stateReseed=true trailReseed=true fieldResubmit=false timelineReset=false "
|
|
"gpuResourceRebuild=false");
|
|
} else if (reseedDecision.action == ParticleViewportReseedAction::SkipInvalidDomain) {
|
|
Log::Info(Event::Render,
|
|
"ParticleDensity: event=VIEWPORT_RESEED reason=ZOOM_OUT action=SKIP "
|
|
"skipReason=INVALID_RESPAWN_DOMAIN startZoom=" +
|
|
std::to_string(reseedDecision.startZoom) + " endZoom=" +
|
|
std::to_string(reseedDecision.endZoom) +
|
|
" stateReseed=false trailReseed=false fieldResubmit=false timelineReset=false "
|
|
"gpuResourceRebuild=false");
|
|
}
|
|
Log::Info(Event::Render,
|
|
"ParticleTrail: event=CAMERA_STABLE_RESUME statePreserved=" +
|
|
std::string(reseedDecision.action == ParticleViewportReseedAction::Reseed ? "false" : "true") +
|
|
" fieldResubmit=false timelineReset=false");
|
|
}
|
|
if (resources->formalDisplay && !resources->trailCameraInvalidated && !resources->viewportDensityLogged) {
|
|
const auto& bounds = resources->respawnBounds;
|
|
const auto& currentFieldBounds = fieldBounds(resources->field);
|
|
Log::Info(Event::Render,
|
|
"ParticleDensity: zoom=" + std::to_string(transformState.getZoom()) +
|
|
" mode=" + std::string(respawnDomain.fallback ? "FIELD_FALLBACK" : "VIEWPORT_RESPAWN") +
|
|
" cameraStable=true margin=" + std::to_string(ViewportRespawnMargin) + " respawnBounds=[" +
|
|
std::to_string(bounds[0]) + "," + std::to_string(bounds[1]) + "," + std::to_string(bounds[2]) +
|
|
"," + std::to_string(bounds[3]) + "] fieldBounds=[" + std::to_string(currentFieldBounds[0]) +
|
|
"," + std::to_string(currentFieldBounds[1]) + "," + std::to_string(currentFieldBounds[2]) + "," +
|
|
std::to_string(currentFieldBounds[3]) +
|
|
"] fallback=" + (respawnDomain.fallback ? "true" : "false") +
|
|
" configuredSlots=" + std::to_string(resources->particleCount));
|
|
resources->viewportDensityLogged = true;
|
|
}
|
|
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,
|
|
gfx::TextureChannelDataType::UnsignedByte,
|
|
"TRAIL");
|
|
}
|
|
if (!resources->trailTargets[0] || !resources->trailTargets[1]) {
|
|
Log::Error(Event::Render, "ParticleGPU: Trail resource creation failed");
|
|
return false;
|
|
}
|
|
resources->trailSize = viewport;
|
|
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::TrailTargetCreateOrResize);
|
|
resources->trailRead = 0;
|
|
resources->trailSeeded = false;
|
|
Log::Info(
|
|
Event::Render,
|
|
"ParticleStateTarget: stateAType=Float stateBType=Float trailAType=UnsignedByte trailBType=UnsignedByte "
|
|
"stateGrid=" +
|
|
std::to_string(resources->stateSize.width) + "x" + std::to_string(resources->stateSize.height) +
|
|
" trailSize=" + std::to_string(viewport.width) + "x" + std::to_string(viewport.height) +
|
|
" resourceGeneration=" + std::to_string(resources->generation));
|
|
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 || resources->trailCameraInvalidated ||
|
|
buildParticlePoints(context,
|
|
resources,
|
|
trailGroup,
|
|
getID() + "-static-bootstrap-trail-particles",
|
|
resources->stateTargets[resources->stateRead]->getTexture(),
|
|
resources->stateTargets[stateWrite]->getTexture(),
|
|
resources->trailTargets[trailWrite]->getTexture(),
|
|
stateWrite,
|
|
trailWrite);
|
|
#if MLN_RENDER_BACKEND_METAL
|
|
const bool endpointCoverageBuilt = !resources->formalDisplay || resources->trailCameraInvalidated ||
|
|
buildParticleEndpointCoverage(
|
|
context,
|
|
resources,
|
|
trailGroup,
|
|
getID() + "-static-bootstrap-trail-particle-endpoints",
|
|
resources->stateTargets[resources->stateRead]->getTexture(),
|
|
resources->stateTargets[stateWrite]->getTexture(),
|
|
resources->trailTargets[trailWrite]->getTexture(),
|
|
stateWrite,
|
|
trailWrite);
|
|
#else
|
|
const bool endpointCoverageBuilt = true;
|
|
#endif
|
|
compositeGroup.clearDrawables();
|
|
bool compositeBuilt = true;
|
|
const auto center = transformState.getLatLng(LatLng::Unwrapped);
|
|
const auto centerWrap = wrapForLongitude(center.longitude());
|
|
const auto firstWrap = resources->formalDisplay ? int16_t{0} : static_cast<int16_t>(centerWrap - 1);
|
|
const auto lastWrap = resources->formalDisplay ? int16_t{0} : static_cast<int16_t>(centerWrap + 1);
|
|
for (int16_t wrap = firstWrap; wrap <= lastWrap; ++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 || !endpointCoverageBuilt || !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) + " field=" + resources->field.fieldID +
|
|
" 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());
|
|
const auto firstWrap = resources->formalDisplay ? int16_t{0} : static_cast<int16_t>(centerWrap - 1);
|
|
const auto lastWrap = resources->formalDisplay ? int16_t{0} : static_cast<int16_t>(centerWrap + 1);
|
|
for (int16_t wrap = firstWrap; wrap <= lastWrap; ++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;
|
|
}
|
|
|
|
// Camera-driven frames may recompose existing weather drawables, but do
|
|
// not advance the shared timeline, State, or Trail before the deadline.
|
|
if (formalSource && (sourceTimeline || sourceUsesV4Projection) && !cadenceAdvanced) {
|
|
auto& compositeGroup = static_cast<LayerGroup&>(*layerGroup);
|
|
compositeGroup.clearDrawables();
|
|
const auto center = transformState.getLatLng(LatLng::Unwrapped);
|
|
const auto centerWrap = wrapForLongitude(center.longitude());
|
|
const auto wrap = static_cast<int16_t>(centerWrap);
|
|
buildQuad(context,
|
|
resources,
|
|
compositeGroup,
|
|
getID() + "-composite-pass-" + std::to_string(wrap),
|
|
resources->trailTargets[resources->trailRead]->getTexture(),
|
|
{},
|
|
resources->maskTexture,
|
|
{},
|
|
0.0f,
|
|
ValidationPass::Composite,
|
|
resources->trailRead,
|
|
resources->trailRead,
|
|
wrap,
|
|
maskMode,
|
|
false);
|
|
return;
|
|
}
|
|
|
|
const auto stateWrite = resources->stateWrite();
|
|
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::ParticleSimulationAdvance);
|
|
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 auto now = std::chrono::steady_clock::now();
|
|
float wallDeltaSeconds = 1.0f / 60.0f;
|
|
if (resources->lastSimulationTick.time_since_epoch().count() != 0) {
|
|
wallDeltaSeconds = std::chrono::duration<float>(now - resources->lastSimulationTick).count();
|
|
}
|
|
resources->lastSimulationTick = now;
|
|
const float trailWallDeltaSeconds = std::clamp(wallDeltaSeconds, 0.0f, 0.250f);
|
|
const float zoom = static_cast<float>(transformState.getZoom());
|
|
const float effectiveVisualSpeedScale = std::clamp(
|
|
VisualSimulationSecondsPerRealSecond * std::exp2(VisualSimulationReferenceZoom - zoom),
|
|
MinVisualSimulationSecondsPerRealSecond,
|
|
MaxVisualSimulationSecondsPerRealSecond);
|
|
const float visualSimulationDeltaSeconds = std::min(
|
|
std::clamp(wallDeltaSeconds, 1.0f / 240.0f, 0.050f) * effectiveVisualSpeedScale, MaxSimulationDeltaSeconds);
|
|
|
|
const bool stateBuilt = buildQuad(context,
|
|
resources,
|
|
stateGroup,
|
|
getID() + "-state-pass",
|
|
resources->stateTargets[resources->stateRead]->getTexture(),
|
|
resources->stateTargets[stateWrite]->getTexture(),
|
|
resources->maskTexture,
|
|
{},
|
|
visualSimulationDeltaSeconds,
|
|
ValidationPass::State,
|
|
resources->stateRead,
|
|
stateWrite,
|
|
0,
|
|
maskMode,
|
|
true,
|
|
trailWallDeltaSeconds,
|
|
effectiveVisualSpeedScale);
|
|
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(),
|
|
trailWallDeltaSeconds,
|
|
ValidationPass::Trail,
|
|
resources->trailRead,
|
|
trailWrite,
|
|
0,
|
|
maskMode,
|
|
true,
|
|
trailWallDeltaSeconds,
|
|
effectiveVisualSpeedScale);
|
|
const bool particlesBuilt = !resources->formalDisplay || resources->trailCameraInvalidated ||
|
|
buildParticlePoints(context,
|
|
resources,
|
|
trailGroup,
|
|
getID() + "-trail-particles",
|
|
resources->stateTargets[resources->stateRead]->getTexture(),
|
|
resources->stateTargets[stateWrite]->getTexture(),
|
|
resources->trailTargets[trailWrite]->getTexture(),
|
|
stateWrite,
|
|
trailWrite);
|
|
#if MLN_RENDER_BACKEND_METAL
|
|
const bool endpointCoverageBuilt = !resources->formalDisplay || resources->trailCameraInvalidated ||
|
|
buildParticleEndpointCoverage(context,
|
|
resources,
|
|
trailGroup,
|
|
getID() + "-trail-particle-endpoints",
|
|
resources->stateTargets[resources->stateRead]->getTexture(),
|
|
resources->stateTargets[stateWrite]->getTexture(),
|
|
resources->trailTargets[trailWrite]->getTexture(),
|
|
stateWrite,
|
|
trailWrite);
|
|
#else
|
|
const bool endpointCoverageBuilt = true;
|
|
#endif
|
|
compositeGroup.clearDrawables();
|
|
bool compositeBuilt = true;
|
|
const auto center = transformState.getLatLng(LatLng::Unwrapped);
|
|
const auto centerWrap = wrapForLongitude(center.longitude());
|
|
const auto firstWrap = resources->formalDisplay ? int16_t{0} : static_cast<int16_t>(centerWrap - 1);
|
|
const auto lastWrap = resources->formalDisplay ? int16_t{0} : static_cast<int16_t>(centerWrap + 1);
|
|
for (int16_t wrap = firstWrap; wrap <= lastWrap; ++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 || !endpointCoverageBuilt || !compositeBuilt) {
|
|
compositeGroup.clearDrawables();
|
|
return;
|
|
}
|
|
|
|
if (formalSource && sourceTimeline) sourceTimelineCadenceRevision = cadenceRevision;
|
|
|
|
// 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
|