feat: establish android particle field rendering loop
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
OpenAI Codex
2026-08-31 10:41:12 +08:00
parent 1ec4470114
commit 0b076c6d52
25 changed files with 816 additions and 63 deletions

View File

@@ -0,0 +1,195 @@
#include <mbgl/particle/field/offline_fixture_particle_adapter.hpp>
#include <rapidjson/document.h>
#include <cmath>
#include <array>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <stdexcept>
namespace mbgl::particle {
namespace {
bool knownFixture(const std::string& fixtureID) {
return fixtureID == "uniform-east" || fixtureID == "uniform-north" || fixtureID == "vortex";
}
std::string readText(const std::filesystem::path& path) {
std::ifstream input(path);
if (!input) throw std::invalid_argument("fixture metadata unavailable: " + path.string());
return {std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
}
std::vector<char> readBinary(const std::filesystem::path& path) {
std::ifstream input(path, std::ios::binary);
if (!input) throw std::invalid_argument("fixture field unavailable: " + path.string());
return {std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
}
const rapidjson::Value& member(const rapidjson::Value& value, const char* name) {
if (!value.HasMember(name)) throw std::invalid_argument(std::string("fixture metadata missing: ") + name);
return value[name];
}
std::string sha256(const std::vector<char>& input) {
constexpr std::array<uint32_t, 64> constants = {
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2};
const auto rotateRight = [](uint32_t value, uint32_t bits) {
return (value >> bits) | (value << (32 - bits));
};
std::vector<uint8_t> bytes(input.begin(), input.end());
const uint64_t bitLength = static_cast<uint64_t>(bytes.size()) * 8;
bytes.push_back(0x80);
while ((bytes.size() % 64) != 56) bytes.push_back(0);
for (int shift = 56; shift >= 0; shift -= 8) bytes.push_back(static_cast<uint8_t>(bitLength >> shift));
std::array<uint32_t, 8> hash = {
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19};
for (size_t offset = 0; offset < bytes.size(); offset += 64) {
std::array<uint32_t, 64> words{};
for (size_t i = 0; i < 16; ++i) {
words[i] = (static_cast<uint32_t>(bytes[offset + i * 4]) << 24) |
(static_cast<uint32_t>(bytes[offset + i * 4 + 1]) << 16) |
(static_cast<uint32_t>(bytes[offset + i * 4 + 2]) << 8) |
static_cast<uint32_t>(bytes[offset + i * 4 + 3]);
}
for (size_t i = 16; i < words.size(); ++i) {
const uint32_t a = rotateRight(words[i - 15], 7) ^ rotateRight(words[i - 15], 18) ^ (words[i - 15] >> 3);
const uint32_t b = rotateRight(words[i - 2], 17) ^ rotateRight(words[i - 2], 19) ^ (words[i - 2] >> 10);
words[i] = words[i - 16] + a + words[i - 7] + b;
}
uint32_t a = hash[0], b = hash[1], c = hash[2], d = hash[3];
uint32_t e = hash[4], f = hash[5], g = hash[6], h = hash[7];
for (size_t i = 0; i < words.size(); ++i) {
const uint32_t sigma1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
const uint32_t choice = (e & f) ^ (~e & g);
const uint32_t temp1 = h + sigma1 + choice + constants[i] + words[i];
const uint32_t sigma0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
const uint32_t majority = (a & b) ^ (a & c) ^ (b & c);
const uint32_t temp2 = sigma0 + majority;
h = g;
g = f;
f = e;
e = d + temp1;
d = c;
c = b;
b = a;
a = temp1 + temp2;
}
hash[0] += a;
hash[1] += b;
hash[2] += c;
hash[3] += d;
hash[4] += e;
hash[5] += f;
hash[6] += g;
hash[7] += h;
}
static constexpr char digits[] = "0123456789abcdef";
std::string result;
result.reserve(64);
for (const uint32_t word : hash) {
for (int shift = 28; shift >= 0; shift -= 4) result.push_back(digits[(word >> shift) & 0x0f]);
}
return result;
}
} // namespace
bool ParticleFieldSnapshot::isValid() const {
if (fixtureID.empty() || fieldType != "WIND" || displayDomain != "GLOBAL" || width == 0 || height == 0 ||
!(west < east) || !(south < north) || velocity.size() != static_cast<size_t>(width) * height * 2) {
return false;
}
for (const float component : velocity) {
if (!std::isfinite(component) || component == noData) {
return false;
}
}
return true;
}
ParticleFieldSnapshot OfflineFixtureParticleAdapter::load(const std::string& fixtureID, const uint64_t generation) {
if (!knownFixture(fixtureID)) {
throw std::invalid_argument("unknown offline particle fixture: " + fixtureID);
}
const auto directory = std::filesystem::path{"particle-fixtures"} / fixtureID;
rapidjson::Document metadata;
metadata.Parse(readText(directory / "metadata.json").c_str());
if (metadata.HasParseError() || !metadata.IsObject() || !member(metadata, "schema").IsString() ||
std::string(member(metadata, "schema").GetString()) != "maplibre-particle-fixture/v1" ||
!member(metadata, "fixtureId").IsString() ||
std::string(member(metadata, "fixtureId").GetString()) != fixtureID ||
!member(metadata, "fieldType").IsString() || !member(metadata, "displayDomain").IsString()) {
throw std::invalid_argument("invalid fixture metadata: " + fixtureID);
}
const auto& grid = member(metadata, "grid");
const auto& bounds = member(metadata, "bounds");
const auto& vector = member(metadata, "vector");
const auto& integrity = member(metadata, "integrity");
if (!grid.IsObject() || !bounds.IsObject() || !vector.IsObject() || !integrity.IsObject() ||
!member(metadata, "timeIndex").IsUint64() || !member(metadata, "crs").IsString() ||
std::string(member(metadata, "crs").GetString()) != "EPSG:4326" || !member(grid, "width").IsUint() ||
!member(grid, "height").IsUint() || !member(bounds, "west").IsNumber() || !member(bounds, "south").IsNumber() ||
!member(bounds, "east").IsNumber() || !member(bounds, "north").IsNumber() ||
!member(grid, "registration").IsString() || !member(grid, "origin").IsString() ||
!member(grid, "rowOrder").IsString() || !member(grid, "columnOrder").IsString() ||
!member(vector, "uPositive").IsString() || !member(vector, "vPositive").IsString() ||
!member(vector, "unit").IsString() || !member(vector, "layout").IsString() ||
!member(vector, "encoding").IsString() || !member(vector, "noData").IsNumber() ||
!member(vector, "file").IsString() || !member(integrity, "fieldByteLength").IsUint64() ||
!member(integrity, "fieldSha256").IsString()) {
throw std::invalid_argument("invalid fixture grid/bounds/encoding: " + fixtureID);
}
if (std::string(grid["registration"].GetString()) != "node" ||
std::string(grid["origin"].GetString()) != "north-west" ||
std::string(grid["rowOrder"].GetString()) != "north-to-south" ||
std::string(grid["columnOrder"].GetString()) != "west-to-east" ||
std::string(vector["uPositive"].GetString()) != "east" ||
std::string(vector["vPositive"].GetString()) != "north" ||
std::string(vector["layout"].GetString()) != "interleaved-uv" ||
std::string(vector["encoding"].GetString()) != "float32-le" ||
std::string(vector["file"].GetString()) != "field.f32") {
throw std::invalid_argument("unsupported fixture v1 layout: " + fixtureID);
}
ParticleFieldSnapshot field;
field.fixtureID = fixtureID;
field.fieldType = member(metadata, "fieldType").GetString();
field.displayDomain = member(metadata, "displayDomain").GetString();
field.width = grid["width"].GetUint();
field.height = grid["height"].GetUint();
field.west = bounds["west"].GetDouble();
field.south = bounds["south"].GetDouble();
field.east = bounds["east"].GetDouble();
field.north = bounds["north"].GetDouble();
field.noData = static_cast<float>(vector["noData"].GetDouble());
field.dataVersion = member(metadata, "dataVersion").GetUint64();
field.generation = generation;
const auto bytes = readBinary(directory / "field.f32");
const size_t expected = static_cast<size_t>(field.width) * field.height * 2 * sizeof(float);
if (bytes.size() != expected || member(integrity, "fieldByteLength").GetUint64() != expected) {
throw std::invalid_argument("fixture field length mismatch: " + fixtureID);
}
if (sha256(bytes) != member(integrity, "fieldSha256").GetString()) {
throw std::invalid_argument("fixture field checksum mismatch: " + fixtureID);
}
field.velocity.resize(expected / sizeof(float));
std::memcpy(field.velocity.data(), bytes.data(), expected);
if (!field.isValid()) {
throw std::invalid_argument("invalid offline particle fixture: " + fixtureID);
}
return field;
}
} // namespace mbgl::particle

View File

@@ -0,0 +1,39 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace mbgl::particle {
// Immutable, backend-neutral Field snapshot used by the first offline particle
// implementation. The renderer owns only a GPU representation of this value.
struct ParticleFieldSnapshot {
std::string fixtureID;
std::string fieldType;
std::string displayDomain;
uint32_t width = 0;
uint32_t height = 0;
double west = -180.0;
double south = -85.0;
double east = 180.0;
double north = 85.0;
float noData = -9999.0f;
uint64_t dataVersion = 0;
uint64_t generation = 0;
// Row-major, little-endian contract decoded as interleaved canonical
// U-east/V-north values. A noData component makes the cell invalid.
std::vector<float> velocity;
bool isValid() const;
};
// Phase-one OfflineFixtureParticleAdapter. Its public contract deliberately
// contains no Android/UI/GPU objects, allowing the same decoded snapshot to be
// used by future Android Vulkan and iOS renderers.
class OfflineFixtureParticleAdapter final {
public:
static ParticleFieldSnapshot load(const std::string& fixtureID, uint64_t generation);
};
} // namespace mbgl::particle

View File

@@ -9,6 +9,7 @@
#include <mbgl/gfx/shader_registry.hpp>
#include <mbgl/gfx/texture2d.hpp>
#include <mbgl/map/transform_state.hpp>
#include <mbgl/particle/field/offline_fixture_particle_adapter.hpp>
#include <mbgl/renderer/buckets/heatmap_bucket.hpp>
#include <mbgl/renderer/change_request.hpp>
#include <mbgl/renderer/layer_group.hpp>
@@ -42,6 +43,7 @@ constexpr Size MaskSize{16, 16};
std::atomic_uint64_t nextGpuGeneration{0};
std::atomic_uint64_t nextMaskGeneration{0};
std::atomic_uint64_t nextFieldGeneration{0};
const style::ParticleValidationLayer::Impl& impl(const Immutable<style::Layer::Impl>& value) {
assert(value->getTypeInfo() == style::ParticleValidationLayer::Impl::staticTypeInfo());
@@ -63,6 +65,34 @@ Size validViewportSize(const Size size) {
return {std::max<uint32_t>(1, size.width), std::max<uint32_t>(1, size.height)};
}
const char* fixtureID(const uint8_t mode) {
switch (mode) {
case 0:
return "uniform-east";
case 1:
return "uniform-north";
default:
return "vortex";
}
}
std::shared_ptr<PremultipliedImage> makeVelocityFieldImage(const particle::ParticleFieldSnapshot& field) {
auto image = std::make_shared<PremultipliedImage>(Size{field.width, field.height});
for (uint32_t y = 0; y < field.height; ++y) {
for (uint32_t x = 0; x < field.width; ++x) {
const auto cell = (static_cast<size_t>(y) * field.width + x) * 2;
const auto pixel = (static_cast<size_t>(y) * field.width + x) * PremultipliedImage::channels;
image->data[pixel + 0] = static_cast<uint8_t>(std::clamp(field.velocity[cell] * 0.5f + 0.5f, 0.0f, 1.0f) *
255.0f);
image->data[pixel + 1] = static_cast<uint8_t>(
std::clamp(field.velocity[cell + 1] * 0.5f + 0.5f, 0.0f, 1.0f) * 255.0f);
image->data[pixel + 2] = 0;
image->data[pixel + 3] = 255;
}
}
return image;
}
// VALIDATION ONLY: deterministic global fixture with a diagonal coast. Alpha
// 255 means OCEAN, alpha 0 means LAND. It is intentionally tiny and is not a
// production coastline source.
@@ -104,6 +134,7 @@ public:
bool stateSeeded = false;
bool trailSeeded = false;
bool pending = false;
bool bootstrapPending = false;
bool contextDestroyed = false;
bool staticFreezeLogged = false;
bool cameraDrawLogged = false;
@@ -111,6 +142,9 @@ public:
bool antimeridianLogged = false;
uint64_t lastCameraLogFrame = 0;
uint64_t maskGeneration = 0;
uint64_t fieldGeneration = 0;
uint64_t gpuFieldGeneration = 0;
uint8_t fixtureMode = 255;
uint8_t loggedMaskMode = 255;
Size trailSize{};
@@ -119,6 +153,10 @@ public:
std::vector<RenderTargetPtr> registeredTargets;
gfx::ShaderProgramBasePtr shader;
gfx::Texture2DPtr maskTexture;
gfx::Texture2DPtr fieldTexture;
gfx::Texture2DPtr stateTexture;
particle::ParticleFieldSnapshot field;
std::shared_ptr<PremultipliedImage> fieldImage;
std::shared_ptr<PremultipliedImage> maskFixture;
std::shared_ptr<gfx::VertexVector<HeatmapTextureLayoutVertex>> vertices;
SegmentVector segments;
@@ -153,7 +191,10 @@ public:
trailRead = trailWrite();
stateSeeded = true;
trailSeeded = true;
++committedFrame;
if (!bootstrapPending) {
++committedFrame;
}
bootstrapPending = false;
pending = false;
}
};
@@ -169,6 +210,8 @@ public:
gfx::Texture2DPtr source_,
gfx::Texture2DPtr target_,
gfx::Texture2DPtr mask_,
gfx::Texture2DPtr stateTexture_,
float simulationStep_,
int16_t worldWrap_,
uint8_t maskMode_)
: state(std::move(state_)),
@@ -178,6 +221,8 @@ public:
source(std::move(source_)),
target(std::move(target_)),
mask(std::move(mask_)),
stateTexture(std::move(stateTexture_)),
simulationStep(simulationStep_),
worldWrap(worldWrap_),
maskMode(maskMode_) {}
@@ -193,9 +238,14 @@ public:
if (mask) {
drawable.setTexture(mask, shaders::idParticleGpuValidationMaskTexture);
}
if (resources->fieldTexture) {
drawable.setTexture(resources->fieldTexture, shaders::idParticleGpuValidationFieldTexture);
}
if (stateTexture) {
drawable.setTexture(stateTexture, shaders::idParticleGpuValidationStateTexture);
}
const bool frozen = pass == ValidationPass::FrozenComposite;
const float frame = static_cast<float>(resources->committedFrame + (frozen ? 0 : 1));
const float frame = simulationStep;
const float markerX = 0.5f + 0.32f * std::sin(frame * 0.071f);
const float markerY = 0.5f + 0.32f * std::cos(frame * 0.053f);
const shaders::ParticleGpuValidationUBO ubo = {
@@ -308,6 +358,8 @@ private:
gfx::Texture2DPtr source;
gfx::Texture2DPtr target;
gfx::Texture2DPtr mask;
gfx::Texture2DPtr stateTexture;
float simulationStep;
int16_t worldWrap;
uint8_t maskMode;
};
@@ -336,6 +388,8 @@ bool buildQuad(gfx::Context& context,
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,
@@ -372,8 +426,14 @@ bool buildQuad(gfx::Context& context,
if (mask) {
builder->setTexture(mask, shaders::idParticleGpuValidationMaskTexture);
}
if (resources->fieldTexture) {
builder->setTexture(resources->fieldTexture, shaders::idParticleGpuValidationFieldTexture);
}
if (stateTexture) {
builder->setTexture(stateTexture, shaders::idParticleGpuValidationStateTexture);
}
builder->addTweaker(std::make_shared<ParticleGpuValidationTweaker>(
resources, pass, readSlot, writeSlot, source, target, mask, worldWrap, maskMode));
resources, pass, readSlot, writeSlot, source, target, mask, stateTexture, simulationStep, worldWrap, maskMode));
// DrawableBuilder owns the current drawable until flush finalizes it.
builder->flush(context);
@@ -389,7 +449,8 @@ bool buildQuad(gfx::Context& context,
} // namespace
RenderParticleValidationLayer::RenderParticleValidationLayer(Immutable<style::ParticleValidationLayer::Impl> impl_)
: RenderLayer(makeMutable<style::ParticleValidationLayerProperties>(impl_, impl_->active, impl_->maskMode)) {
: RenderLayer(makeMutable<style::ParticleValidationLayerProperties>(
impl_, impl_->active, impl_->maskMode, impl_->fixtureMode)) {
Log::Info(
Event::Render,
"ParticleValidation: RenderLayer create id=" + getID() + " source-less=true pass=translucent stage=30-02");
@@ -407,16 +468,17 @@ RenderParticleValidationLayer::~RenderParticleValidationLayer() {
void RenderParticleValidationLayer::evaluate(const PropertyEvaluationParameters&) {
const bool active = impl(baseImpl).active;
const uint8_t maskMode = impl(baseImpl).maskMode;
const uint8_t fixtureMode = impl(baseImpl).fixtureMode;
auto properties = makeMutable<style::ParticleValidationLayerProperties>(
staticImmutableCast<style::ParticleValidationLayer::Impl>(baseImpl), active, maskMode);
staticImmutableCast<style::ParticleValidationLayer::Impl>(baseImpl), active, maskMode, fixtureMode);
passes = RenderPass::Translucent;
properties->renderPasses = mbgl::underlying_type(passes);
evaluatedProperties = std::move(properties);
Log::Info(Event::Render,
"ParticleValidation: state=" + std::string(active ? "ACTIVE" : "STATIC") +
" maskMode=" + std::to_string(maskMode) + " id=" + getID() +
" layerIndex=" + std::to_string(getLayerIndex()) + " stage=30-03");
" maskMode=" + std::to_string(maskMode) + " id=" + getID() + " fixture=" + fixtureID(fixtureMode) +
" layerIndex=" + std::to_string(getLayerIndex()) + " stage=40-01");
}
bool RenderParticleValidationLayer::needsRepaint() const {
@@ -471,6 +533,7 @@ void RenderParticleValidationLayer::update(gfx::ShaderRegistry& shaders,
const auto& properties = static_cast<const style::ParticleValidationLayerProperties&>(*evaluatedProperties);
const bool active = properties.active;
const uint8_t maskMode = properties.maskMode;
const uint8_t fixtureMode = properties.fixtureMode;
auto resources = gpuValidation;
if (resources && resources->contextDestroyed) {
// VALIDATION ONLY: follow the 30-02 resource-generation rule. No stale
@@ -479,13 +542,6 @@ void RenderParticleValidationLayer::update(gfx::ShaderRegistry& shaders,
resources.reset();
}
if (!resources) {
if (!active) {
// STATIC without a preceding ACTIVE frame has no trail to show. Do not create,
// clear, or advance offscreen resources merely because the map redraws.
static_cast<LayerGroup&>(*layerGroup).clearDrawables();
return;
}
auto owned = std::make_shared<ParticleGpuValidationResources>();
owned->generation = ++nextGpuGeneration;
owned->shader = context.getGenericShader(shaders, ShaderName);
@@ -511,11 +567,51 @@ void RenderParticleValidationLayer::update(gfx::ShaderRegistry& shaders,
.wrapU = gfx::TextureWrapType::Clamp,
.wrapV = gfx::TextureWrapType::Clamp});
owned->maskGeneration = ++nextMaskGeneration;
try {
owned->fieldGeneration = ++nextFieldGeneration;
owned->field = particle::OfflineFixtureParticleAdapter::load(fixtureID(fixtureMode),
owned->fieldGeneration);
owned->fieldImage = makeVelocityFieldImage(owned->field);
owned->fieldTexture = context.createTexture2D();
if (!owned->fieldImage || !owned->fieldTexture) {
Log::Error(Event::Render, "ParticleGPU: field resource creation failed");
return;
}
owned->fieldTexture->setImage(owned->fieldImage);
owned->fieldTexture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Nearest,
.wrapU = gfx::TextureWrapType::Repeat,
.wrapV = gfx::TextureWrapType::Clamp});
owned->gpuFieldGeneration = owned->fieldGeneration;
owned->fixtureMode = fixtureMode;
} catch (const std::exception& error) {
Log::Error(Event::Render, "ParticleField: fixture load failed: " + std::string(error.what()));
return;
}
gpuValidation = std::move(owned);
resources = gpuValidation;
Log::Info(Event::Render,
"ParticleValidation30_02: State A/B create generation=" + std::to_string(resources->generation) +
" size=16x16 independent=true");
Log::Info(
Event::Render,
"ParticleField: fixture loaded id=" + resources->field.fixtureID + " type=" + resources->field.fieldType +
" bounds=[" + std::to_string(resources->field.west) + ',' + std::to_string(resources->field.east) +
"]x[" + std::to_string(resources->field.south) + ',' + std::to_string(resources->field.north) +
"] grid=" + std::to_string(resources->field.width) + 'x' + std::to_string(resources->field.height) +
" dataVersion=" + std::to_string(resources->field.dataVersion) +
" fieldGeneration=" + std::to_string(resources->fieldGeneration));
Log::Info(Event::Render,
"ParticleField: fixture validated id=" + resources->field.fixtureID +
" noData=" + std::to_string(resources->field.noData) +
" velocityCount=" + std::to_string(resources->field.velocity.size()));
Log::Info(Event::Render,
"ParticleField: created id=" + resources->field.fixtureID +
" generation=" + std::to_string(resources->fieldGeneration));
Log::Info(Event::Render,
"ParticleGPU: field upload reason=RESOURCE_RECREATE fixture=" + resources->field.fixtureID +
" fieldGeneration=" + std::to_string(resources->fieldGeneration) +
" gpuFieldGeneration=" + std::to_string(resources->gpuFieldGeneration) +
" resourceGeneration=" + std::to_string(resources->generation));
Log::Info(
Event::Render,
"ParticleValidation30_03: mask fixture ready id=coastal-mask-validation-v1 bounds=[-180,180]x[-85,85] "
@@ -525,6 +621,41 @@ void RenderParticleValidationLayer::update(gfx::ShaderRegistry& shaders,
"ParticleValidation30_03: anchors A=0,0 B=0,-90 C=0,-172.8 D=0,172.8 canonical=Mercator-z0");
}
if (resources->fixtureMode != fixtureMode) {
try {
const auto fieldGeneration = ++nextFieldGeneration;
auto field = particle::OfflineFixtureParticleAdapter::load(fixtureID(fixtureMode), fieldGeneration);
auto image = makeVelocityFieldImage(field);
auto texture = context.createTexture2D();
if (!image || !texture) {
Log::Error(Event::Render, "ParticleGPU: field switch resource creation failed");
return;
}
texture->setImage(image);
texture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Nearest,
.wrapU = gfx::TextureWrapType::Repeat,
.wrapV = gfx::TextureWrapType::Clamp});
resources->field = std::move(field);
resources->fieldImage = std::move(image);
resources->fieldTexture = std::move(texture);
resources->fieldGeneration = fieldGeneration;
resources->gpuFieldGeneration = fieldGeneration;
resources->fixtureMode = fixtureMode;
resources->resetSequence();
Log::Info(Event::Render,
"ParticleRender: fixture switched id=" + resources->field.fixtureID +
" fieldGeneration=" + std::to_string(resources->fieldGeneration) + " gpuFieldGeneration=" +
std::to_string(resources->gpuFieldGeneration) + " upload reason=FIELD_CHANGED trail=cleared");
Log::Info(Event::Render,
"ParticleField: fixture validated id=" + resources->field.fixtureID +
" noData=" + std::to_string(resources->field.noData) +
" velocityCount=" + std::to_string(resources->field.velocity.size()));
} catch (const std::exception& error) {
Log::Error(Event::Render, "ParticleField: fixture switch failed: " + std::string(error.what()));
return;
}
}
if (resources->loggedMaskMode != maskMode) {
Log::Info(Event::Render,
"ParticleValidation30_03: mask mode=" + std::to_string(maskMode) + " consumer=" +
@@ -536,13 +667,119 @@ void RenderParticleValidationLayer::update(gfx::ShaderRegistry& shaders,
removeTargets(resources->registeredTargets, changes);
resources->registeredTargets.clear();
const auto viewport = validViewportSize(transformState.getSize());
const auto ensureTrailTargets = [&] {
if (resources->trailTargets[0] && resources->trailTargets[1] && resources->trailSize == viewport) {
return true;
}
const auto previousSize = resources->trailSize;
resources->trailTargets = {};
for (uint8_t i = 0; i < 2; ++i) {
resources->trailTargets[i] = createValidationTarget(
context, viewport, getID() + "-trail-" + slotName(i), gfx::TextureFilterType::Linear);
}
if (!resources->trailTargets[0] || !resources->trailTargets[1]) {
Log::Error(Event::Render, "ParticleGPU: Trail resource creation failed");
return false;
}
resources->trailSize = viewport;
resources->trailRead = 0;
resources->trailSeeded = false;
Log::Info(Event::Render,
"ParticleGPU: Trail A/B " + std::string(previousSize.isEmpty() ? "create" : "resize") +
" resourceGeneration=" + std::to_string(resources->generation) +
" old=" + std::to_string(previousSize.width) + "x" + std::to_string(previousSize.height) +
" new=" + std::to_string(viewport.width) + "x" + std::to_string(viewport.height) +
" StatePreserved=true");
return true;
};
if (!active) {
// Map gestures must still redraw the main pass, but STATIC is a simulation freeze:
// no State/Trail RenderTarget is registered, no pending swap survives, and the
// composite samples the last committed Trail slot with the current map frame.
// 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->trailSeeded && resources->trailTargets[resources->trailRead]) {
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);
compositeGroup.clearDrawables();
bool compositeBuilt = true;
const auto center = transformState.getLatLng(LatLng::Unwrapped);
const auto centerWrap = wrapForLongitude(center.longitude());
for (int16_t wrap = static_cast<int16_t>(centerWrap - 1); wrap <= centerWrap + 1; ++wrap) {
compositeBuilt = buildQuad(context,
resources,
compositeGroup,
getID() + "-static-bootstrap-composite-pass-" + std::to_string(wrap),
resources->trailTargets[trailWrite]->getTexture(),
{},
resources->maskTexture,
{},
0.0f,
ValidationPass::Composite,
resources->trailRead,
trailWrite,
wrap,
maskMode,
false) &&
compositeBuilt;
}
if (!stateBuilt || !trailBuilt || !compositeBuilt) {
compositeGroup.clearDrawables();
return;
}
resources->registeredTargets.emplace_back(resources->stateTargets[resources->stateRead]);
resources->registeredTargets.emplace_back(resources->stateTargets[stateWrite]);
resources->registeredTargets.emplace_back(resources->trailTargets[resources->trailRead]);
resources->registeredTargets.emplace_back(resources->trailTargets[trailWrite]);
for (const auto& target : resources->registeredTargets) {
changes.emplace_back(std::make_unique<AddRenderTargetRequest>(target));
}
resources->bootstrapPending = true;
resources->pending = true;
Log::Info(Event::Render,
"ParticleRender: STATIC bootstrap scheduled resourceGeneration=" +
std::to_string(resources->generation) + " fixture=" + resources->field.fixtureID +
" simulationFrame=" + std::to_string(resources->committedFrame));
return;
}
if (resources->trailTargets[resources->trailRead]) {
compositeGroup.clearDrawables();
const auto center = transformState.getLatLng(LatLng::Unwrapped);
const auto centerWrap = wrapForLongitude(center.longitude());
@@ -554,6 +791,8 @@ void RenderParticleValidationLayer::update(gfx::ShaderRegistry& shaders,
resources->trailTargets[resources->trailRead]->getTexture(),
{},
resources->maskTexture,
{},
0.0f,
ValidationPass::FrozenComposite,
resources->trailRead,
resources->trailRead,
@@ -567,28 +806,8 @@ void RenderParticleValidationLayer::update(gfx::ShaderRegistry& shaders,
return;
}
resources->staticFreezeLogged = false;
const auto viewport = validViewportSize(transformState.getSize());
if (!resources->trailTargets[0] || !resources->trailTargets[1] || resources->trailSize != viewport) {
const auto previousSize = resources->trailSize;
resources->trailTargets = {};
for (uint8_t i = 0; i < 2; ++i) {
resources->trailTargets[i] = createValidationTarget(
context, viewport, getID() + "-trail-" + slotName(i), gfx::TextureFilterType::Linear);
}
if (!resources->trailTargets[0] || !resources->trailTargets[1]) {
Log::Error(Event::Render, "ParticleValidation30_02: Trail resource creation failed");
return;
}
resources->trailSize = viewport;
resources->trailRead = 0;
resources->trailSeeded = false;
Log::Info(Event::Render,
"ParticleValidation30_02: Trail A/B " + std::string(previousSize.isEmpty() ? "create" : "resize") +
" generation=" + 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");
if (!ensureTrailTargets()) {
return;
}
const auto stateWrite = resources->stateWrite();
@@ -604,6 +823,8 @@ void RenderParticleValidationLayer::update(gfx::ShaderRegistry& shaders,
resources->stateTargets[resources->stateRead]->getTexture(),
resources->stateTargets[stateWrite]->getTexture(),
resources->maskTexture,
{},
1.0f,
ValidationPass::State,
resources->stateRead,
stateWrite,
@@ -617,6 +838,8 @@ void RenderParticleValidationLayer::update(gfx::ShaderRegistry& shaders,
resources->trailTargets[resources->trailRead]->getTexture(),
resources->trailTargets[trailWrite]->getTexture(),
resources->maskTexture,
resources->stateTargets[stateWrite]->getTexture(),
1.0f,
ValidationPass::Trail,
resources->trailRead,
trailWrite,
@@ -635,6 +858,8 @@ void RenderParticleValidationLayer::update(gfx::ShaderRegistry& shaders,
resources->trailTargets[trailWrite]->getTexture(),
{},
resources->maskTexture,
{},
0.0f,
ValidationPass::Composite,
resources->trailRead,
trailWrite,

View File

@@ -299,6 +299,8 @@ const std::vector<AttributeInfo> ParticleGpuValidationShaderInfo::attributes = {
const std::vector<TextureInfo> ParticleGpuValidationShaderInfo::textures = {
TextureInfo{"u_previous", idParticleGpuValidationPreviousTexture},
TextureInfo{"u_mask_texture", idParticleGpuValidationMaskTexture},
TextureInfo{"u_field_texture", idParticleGpuValidationFieldTexture},
TextureInfo{"u_state_texture", idParticleGpuValidationStateTexture},
};
// Hillshade Prepare

View File

@@ -14,6 +14,7 @@ namespace style {
namespace {
constexpr const char* ActiveProperty = "particle-validation-active";
constexpr const char* MaskModeProperty = "particle-validation-mask-mode";
constexpr const char* FixtureModeProperty = "particle-fixture-mode";
const LayerTypeInfo typeInfo{.type = "particle-validation",
.source = LayerTypeInfo::Source::NotRequired,
@@ -66,6 +67,10 @@ uint8_t ParticleValidationLayer::getMaskMode() const {
return impl().maskMode;
}
uint8_t ParticleValidationLayer::getFixtureMode() const {
return impl().fixtureMode;
}
void ParticleValidationLayer::setActive(bool active) {
if (active == isActive()) {
return;
@@ -86,6 +91,16 @@ void ParticleValidationLayer::setMaskMode(uint8_t mode) {
observer->onLayerChanged(*this);
}
void ParticleValidationLayer::setFixtureMode(uint8_t mode) {
if (mode > 2 || mode == getFixtureMode()) {
return;
}
auto impl_ = mutableImpl();
impl_->fixtureMode = mode;
baseImpl = std::move(impl_);
observer->onLayerChanged(*this);
}
std::optional<conversion::Error> ParticleValidationLayer::setPropertyInternal(const std::string& name,
const conversion::Convertible& value) {
if (name == MaskModeProperty) {
@@ -97,6 +112,15 @@ std::optional<conversion::Error> ParticleValidationLayer::setPropertyInternal(co
setMaskMode(static_cast<uint8_t>(*converted));
return std::nullopt;
}
if (name == FixtureModeProperty) {
conversion::Error error;
auto converted = conversion::convert<float>(value, error);
if (!converted || *converted < 0 || *converted > 2 || std::floor(*converted) != *converted) {
return conversion::Error{"particle-fixture-mode must be 0, 1, or 2"};
}
setFixtureMode(static_cast<uint8_t>(*converted));
return std::nullopt;
}
if (name != ActiveProperty) {
return conversion::Error{"layer doesn't support this property"};
}
@@ -116,6 +140,9 @@ StyleProperty ParticleValidationLayer::getProperty(const std::string& name) cons
if (name == MaskModeProperty) {
return StyleProperty(Value{static_cast<uint64_t>(getMaskMode())}, StyleProperty::Kind::Constant);
}
if (name == FixtureModeProperty) {
return StyleProperty(Value{static_cast<uint64_t>(getFixtureMode())}, StyleProperty::Kind::Constant);
}
return {};
}
@@ -123,6 +150,7 @@ Value ParticleValidationLayer::serialize() const {
auto result = Layer::serialize();
serializeProperty(result, getProperty(ActiveProperty), ActiveProperty, true);
serializeProperty(result, getProperty(MaskModeProperty), MaskModeProperty, true);
serializeProperty(result, getProperty(FixtureModeProperty), FixtureModeProperty, true);
return result;
}

View File

@@ -19,21 +19,28 @@ public:
bool active = true;
// VALIDATION ONLY: 0=GLOBAL, 1=OCEAN_ONLY_A, 2=OCEAN_ONLY_B.
uint8_t maskMode = 0;
// 40-01 offline Field selection: 0=uniform-east, 1=uniform-north, 2=vortex.
uint8_t fixtureMode = 0;
DECLARE_LAYER_TYPE_INFO;
};
class ParticleValidationLayerProperties final : public LayerProperties {
public:
ParticleValidationLayerProperties(Immutable<ParticleValidationLayer::Impl> impl, bool active_, uint8_t maskMode_)
ParticleValidationLayerProperties(Immutable<ParticleValidationLayer::Impl> impl,
bool active_,
uint8_t maskMode_,
uint8_t fixtureMode_)
: LayerProperties(std::move(impl)),
active(active_),
maskMode(maskMode_) {}
maskMode(maskMode_),
fixtureMode(fixtureMode_) {}
expression::Dependency getDependencies() const noexcept override { return expression::Dependency::None; }
bool active;
uint8_t maskMode;
uint8_t fixtureMode;
};
} // namespace style