fix(step097): complete A2 clock and GPU field correctness
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
This commit is contained in:
@@ -101,6 +101,8 @@ struct ParticleFieldSnapshot {
|
||||
std::vector<float> vector;
|
||||
std::vector<uint8_t> valid;
|
||||
std::optional<VectorSemantics> vectorSemantics;
|
||||
// Internal interpretation identity. Never part of the public field ABI.
|
||||
std::string gpuEncodingIdentity;
|
||||
|
||||
bool isValid() const;
|
||||
bool isTimeInterpolationCompatibleWith(const ParticleFieldSnapshot&) const;
|
||||
@@ -120,6 +122,28 @@ inline bool sameFieldGpuContent(const ParticleFieldSnapshot& current, const Part
|
||||
current.vector == next.vector && current.valid == next.valid;
|
||||
}
|
||||
|
||||
// A GPU slot may be reused only for the same immutable endpoint. A matching
|
||||
// producer revision alone is insufficient, and equal bytes at another valid
|
||||
// time must not silently inherit the previous endpoint's provenance.
|
||||
inline bool sameFieldGpuEndpoint(const ParticleFieldSnapshot& a, const ParticleFieldSnapshot& b) {
|
||||
return a.fieldID == b.fieldID && a.generation == b.generation && a.dataVersion == b.dataVersion &&
|
||||
a.validTime == b.validTime && a.fieldType == b.fieldType && a.displayDomain == b.displayDomain &&
|
||||
a.vectorSemantics == b.vectorSemantics && a.noData == b.noData &&
|
||||
a.fieldSha256 == b.fieldSha256 && a.gpuEncodingIdentity == b.gpuEncodingIdentity &&
|
||||
a.width == b.width && a.height == b.height && a.west == b.west && a.south == b.south &&
|
||||
a.east == b.east && a.north == b.north && a.vector == b.vector && a.valid == b.valid;
|
||||
}
|
||||
|
||||
enum class GpuEndpointSource : uint8_t { Upload, OldT0, OldT1 };
|
||||
|
||||
inline GpuEndpointSource gpuEndpointSource(const ParticleFieldSnapshot& oldT0,
|
||||
const ParticleFieldSnapshot& oldT1,
|
||||
const ParticleFieldSnapshot& wanted) {
|
||||
if (sameFieldGpuEndpoint(oldT0, wanted)) return GpuEndpointSource::OldT0;
|
||||
if (sameFieldGpuEndpoint(oldT1, wanted)) return GpuEndpointSource::OldT1;
|
||||
return GpuEndpointSource::Upload;
|
||||
}
|
||||
|
||||
struct ParticleFieldTimeWindow {
|
||||
ParticleFieldSnapshot t0;
|
||||
ParticleFieldSnapshot t1;
|
||||
|
||||
33
include/mbgl/particle/field/particle_gpu_math.hpp
Normal file
33
include/mbgl/particle/field/particle_gpu_math.hpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
namespace mbgl::particle {
|
||||
|
||||
// Internal RGBA8 UNORM field representation. Nearest sampling and highp
|
||||
// shader arithmetic reconstruct the byte with floor(sample*255 + 0.5).
|
||||
inline uint8_t encodeSignedVelocity8(const float value, const float scale, const bool valid) {
|
||||
if (!valid || !std::isfinite(value) || !std::isfinite(scale) || scale <= 0.0f) return 128;
|
||||
return static_cast<uint8_t>(128 + static_cast<int>(
|
||||
std::lround(std::clamp(value / scale, -1.0f, 1.0f) * 127.0f)));
|
||||
}
|
||||
|
||||
inline float decodeSignedVelocity8(const uint8_t byte, const float scale) {
|
||||
return static_cast<float>(static_cast<int>(byte) - 128) * scale / 127.0f;
|
||||
}
|
||||
|
||||
inline std::array<double, 2> mercatorVelocityDelta(const double eastMps,
|
||||
const double northMps,
|
||||
const double latitudeDegrees,
|
||||
const double visualSeconds) {
|
||||
constexpr double circumference = 40075016.68557849;
|
||||
constexpr double radiansPerDegree = 3.14159265358979323846 / 180.0;
|
||||
const double cosine = std::max(std::cos(latitudeDegrees * radiansPerDegree), 0.01);
|
||||
return {{eastMps * visualSeconds / (circumference * cosine),
|
||||
-northMps * visualSeconds / (circumference * cosine)}};
|
||||
}
|
||||
|
||||
} // namespace mbgl::particle
|
||||
@@ -34,8 +34,8 @@ layout (std140) uniform ParticleGpuValidationUBO {
|
||||
|
||||
uniform sampler2D u_state_texture;
|
||||
uniform sampler2D u_previous;
|
||||
uniform sampler2D u_field_texture;
|
||||
uniform sampler2D u_next_field_texture;
|
||||
uniform highp sampler2D u_field_texture;
|
||||
uniform highp sampler2D u_next_field_texture;
|
||||
|
||||
vec2 fieldUv(vec2 world) {
|
||||
return vec2((world.x - u_field_bounds.x) / (u_field_bounds.z - u_field_bounds.x),
|
||||
@@ -47,8 +47,8 @@ bool validFieldUv(vec2 uv) {
|
||||
}
|
||||
|
||||
bool resolveAdvection(vec4 field0, vec4 field1, out vec2 advection) {
|
||||
vec2 mixed = mix((field0.rg * 2.0 - 1.0) * u_field_scale.x,
|
||||
(field1.rg * 2.0 - 1.0) * u_field_scale.y,
|
||||
vec2 mixed = mix(((floor(field0.rg * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.x,
|
||||
((floor(field1.rg * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.y,
|
||||
u_marker.x);
|
||||
if (any(isnan(mixed)) || any(greaterThan(abs(mixed), vec2(3.4e38)))) return false;
|
||||
if (u_advection.x > 0.5) {
|
||||
@@ -116,7 +116,11 @@ void main() {
|
||||
}
|
||||
}
|
||||
)";
|
||||
static constexpr const char* fragment = R"(layout (std140) uniform ParticleGpuValidationUBO {
|
||||
static constexpr const char* fragment = R"(// The shared GL prelude defaults fragment float to mediump. Byte reconstruction
|
||||
// from RGBA8 UNORM and Mercator state require highp on ES 3.x.
|
||||
precision highp float;
|
||||
|
||||
layout (std140) uniform ParticleGpuValidationUBO {
|
||||
highp vec4 u_pass;
|
||||
highp vec4 u_marker;
|
||||
highp vec4 u_mask;
|
||||
@@ -135,8 +139,8 @@ void main() {
|
||||
|
||||
uniform sampler2D u_previous;
|
||||
uniform sampler2D u_mask_texture;
|
||||
uniform sampler2D u_field_texture;
|
||||
uniform sampler2D u_next_field_texture;
|
||||
uniform highp sampler2D u_field_texture;
|
||||
uniform highp sampler2D u_next_field_texture;
|
||||
uniform sampler2D u_state_texture;
|
||||
in vec2 v_uv;
|
||||
in vec2 v_world;
|
||||
@@ -195,8 +199,8 @@ bool fieldAdvectionAt(vec2 position, out vec2 advection) {
|
||||
vec2 uv = fieldUv(position);
|
||||
vec4 field0 = texture(u_field_texture, uv);
|
||||
vec4 field1 = texture(u_next_field_texture, uv);
|
||||
vec2 mixed = mix((field0.rg * 2.0 - 1.0) * u_field_scale.x,
|
||||
(field1.rg * 2.0 - 1.0) * u_field_scale.y,
|
||||
vec2 mixed = mix(((floor(field0.rg * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.x,
|
||||
((floor(field1.rg * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.y,
|
||||
u_marker.x);
|
||||
if (!validFieldUv(uv) || field0.a < 0.5 || field1.a < 0.5 || any(isnan(mixed)) ||
|
||||
any(greaterThan(abs(mixed), vec2(3.4e38)))) return false;
|
||||
@@ -236,14 +240,14 @@ void main() {
|
||||
if (field0.a < 0.5 || field1.a < 0.5) discard;
|
||||
float fieldValue;
|
||||
if (u_weather_ramp.y > 0.5) {
|
||||
vec2 vectorValue = mix((field0.rg * 2.0 - 1.0) * u_field_scale.x,
|
||||
(field1.rg * 2.0 - 1.0) * u_field_scale.y,
|
||||
vec2 vectorValue = mix(((floor(field0.rg * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.x,
|
||||
((floor(field1.rg * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.y,
|
||||
u_marker.x);
|
||||
if (any(isnan(vectorValue))) discard;
|
||||
fieldValue = length(vectorValue);
|
||||
} else {
|
||||
fieldValue = mix((field0.r * 2.0 - 1.0) * u_field_scale.x,
|
||||
(field1.r * 2.0 - 1.0) * u_field_scale.y,
|
||||
fieldValue = mix(((floor(field0.r * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.x,
|
||||
((floor(field1.r * 255.0 + 0.5) - 128.0) / 127.0) * u_field_scale.y,
|
||||
u_marker.x);
|
||||
if (isnan(fieldValue)) discard;
|
||||
}
|
||||
@@ -286,7 +290,7 @@ void main() {
|
||||
|
||||
if (!respawn) {
|
||||
float latitude = mercatorLatitudeRadians(position.y);
|
||||
vec2 displacement = vec2(advection.x * u_pass.y / EARTH_CIRCUMFERENCE_METERS,
|
||||
vec2 displacement = vec2(advection.x * u_pass.y / (EARTH_CIRCUMFERENCE_METERS * max(cos(latitude), 0.01)),
|
||||
-advection.y * u_pass.y /
|
||||
(EARTH_CIRCUMFERENCE_METERS * max(cos(latitude), 0.01)));
|
||||
vec2 nextPosition = position + displacement;
|
||||
@@ -337,10 +341,10 @@ void main() {
|
||||
// U/V remain canonical metres per second. u_pass.y is the explicit
|
||||
// visual simulation delta in seconds; the validation clock may run
|
||||
// faster than wall time, but it never changes the field contract.
|
||||
// Web Mercator x spans Earth's circumference; y additionally scales
|
||||
// with sec(latitude). V is north-positive while world y is southward.
|
||||
// Both east and north metres scale with sec(latitude) in Web Mercator.
|
||||
// V is north-positive while world y is southward.
|
||||
float latitude = mercatorLatitudeRadians(position.y);
|
||||
float dx = advection.x * u_pass.y / EARTH_CIRCUMFERENCE_METERS;
|
||||
float dx = advection.x * u_pass.y / (EARTH_CIRCUMFERENCE_METERS * max(cos(latitude), 0.01));
|
||||
float dy = -advection.y * u_pass.y /
|
||||
(EARTH_CIRCUMFERENCE_METERS * max(cos(latitude), 0.01));
|
||||
position = position + vec2(dx, dy);
|
||||
|
||||
@@ -46,7 +46,7 @@ float2 fieldUv(float2 world, device const ParticleGpuValidationUBO& u) {
|
||||
bool validFieldUv(float2 uv) { return all(uv >= float2(0.0)) && all(uv <= float2(1.0)); }
|
||||
bool finite2(float2 value) { return all(isfinite(value)) && all(abs(value) <= float2(3.4e38)); }
|
||||
bool resolveAdvection(float4 a, float4 b, device const ParticleGpuValidationUBO& u, thread float2& out) {
|
||||
float2 mixed=mix((a.rg*2.0-1.0)*u.fieldScale.x,(b.rg*2.0-1.0)*u.fieldScale.y,u.marker.x);
|
||||
float2 mixed=mix(((floor(a.rg*255.0+0.5)-128.0)/127.0)*u.fieldScale.x,((floor(b.rg*255.0+0.5)-128.0)/127.0)*u.fieldScale.y,u.marker.x);
|
||||
if (!finite2(mixed)) return false;
|
||||
if (u.advection.x > .5) { float l=length(mixed); if (!isfinite(l)||l>3.4e38||l<=u.advection.z) return false; out=mixed/l*u.advection.y; }
|
||||
else { out=mixed; return length(out)>0.00001; }
|
||||
@@ -126,12 +126,12 @@ float4 fragment fragmentMain(FragmentStage in [[stage_in]], device const Particl
|
||||
sampler previousSampler [[sampler(0)]], sampler maskSampler [[sampler(1)]], sampler fieldSampler [[sampler(2)]], sampler nextFieldSampler [[sampler(3)]], sampler stateSampler [[sampler(4)]]) {
|
||||
float mode=u.pass.x;
|
||||
if(mode>5.5){if(in.valid<.5)discard_fragment();float speed=length(in.velocity);return float4(1,1,clamp(speed/30.,0.,1.),1);}
|
||||
if(mode>4.5){float2 uv=fieldUv(in.world,u); if(!validFieldUv(uv)) discard_fragment(); float4 a=field.sample(fieldSampler,uv),b=nextField.sample(nextFieldSampler,uv);if(a.a<.5||b.a<.5)discard_fragment();float value=u.weatherRamp.y>.5?length(mix((a.rg*2.-1.)*u.fieldScale.x,(b.rg*2.-1.)*u.fieldScale.y,u.marker.x)):mix((a.r*2.-1.)*u.fieldScale.x,(b.r*2.-1.)*u.fieldScale.y,u.marker.x);if(!isfinite(value))discard_fragment();float4 c=ramp(value,u);float alpha=c.a*u.visual.x;return float4(c.rgb*alpha,alpha);}
|
||||
if(mode>4.5){float2 uv=fieldUv(in.world,u); if(!validFieldUv(uv)) discard_fragment(); float4 a=field.sample(fieldSampler,uv),b=nextField.sample(nextFieldSampler,uv);if(a.a<.5||b.a<.5)discard_fragment();float value=u.weatherRamp.y>.5?length(mix(((floor(a.rg*255.+.5)-128.)/127.)*u.fieldScale.x,((floor(b.rg*255.+.5)-128.)/127.)*u.fieldScale.y,u.marker.x)):mix(((floor(a.r*255.+.5)-128.)/127.)*u.fieldScale.x,((floor(b.r*255.+.5)-128.)/127.)*u.fieldScale.y,u.marker.x);if(!isfinite(value))discard_fragment();float4 c=ramp(value,u);float alpha=c.a*u.visual.x;return float4(c.rgb*alpha,alpha);}
|
||||
float4 old=previous.sample(previousSampler,in.uv);
|
||||
if(mode<.5){
|
||||
float2 position=u.mask.w>.5?mix(u.fieldBounds.xy,u.fieldBounds.zw,in.uv):old.rg; float age=old.b; bool initial=u.mask.w>.5; bool respawn=initial; float2 advection; float maxAge=u.marker.z*mix(.75,1.25,particleHash(in.uv+float2(17.31,83.17)));
|
||||
if(u.mask.z>.5){age=initial?particleHash(in.uv+float2(71.19,29.47))*maxAge:old.b+u.pass.w;float2 uv=fieldUv(position,u);float4 a=field.sample(fieldSampler,uv),b=nextField.sample(nextFieldSampler,uv);bool ok=validFieldUv(uv)&&a.a>=.5&&b.a>=.5&&resolveAdvection(a,b,u,advection);respawn=initial||!ok||age>=maxAge||(u.fieldScale.z>.5&&!initial&&(position.x<u.respawnBounds.x||position.x>u.respawnBounds.z||position.y<u.respawnBounds.y||position.y>u.respawnBounds.w));if(!respawn){float lat=mercatorLatitudeRadians(position.y);float2 n=position+float2(advection.x*u.pass.y/40075016.68557849,-advection.y*u.pass.y/(40075016.68557849*max(cos(lat),.01)));float2 nuv=fieldUv(n,u);float4 na=field.sample(fieldSampler,nuv),nb=nextField.sample(nextFieldSampler,nuv);if(validFieldUv(nuv)&&na.a>=.5&&nb.a>=.5&&resolveAdvection(na,nb,u,advection))position=n;else respawn=true;}if(respawn){bool found=false;for(int i=0;i<3;i++){float2 c=respawnSeed(in.uv,old.rg,float(i),u);float2 cuv=fieldUv(c,u);float4 ca=field.sample(fieldSampler,cuv),cb=nextField.sample(nextFieldSampler,cuv);if(validFieldUv(cuv)&&ca.a>=.5&&cb.a>=.5&&resolveAdvection(ca,cb,u,advection)){position=c;found=true;break;}}if(!found)position=respawnSeed(in.uv,old.rg,3.,u);if(!initial)age=0.;}return float4(position.x,position.y,age,respawn?0.:1.);}
|
||||
float2 uv=fieldUv(position,u);float4 a=field.sample(fieldSampler,uv),b=nextField.sample(nextFieldSampler,uv);if(!validFieldUv(uv)||a.a<.5||b.a<.5||!resolveAdvection(a,b,u,advection)){position=mix(u.fieldBounds.xy,u.fieldBounds.zw,in.uv);advection=float2(0);}float lat=mercatorLatitudeRadians(position.y);position+=float2(advection.x*u.pass.y/40075016.68557849,-advection.y*u.pass.y/(40075016.68557849*max(cos(lat),.01)));if(!validFieldUv(fieldUv(position,u))||!finite2(advection))position=mix(u.fieldBounds.xy,u.fieldBounds.zw,in.uv);return float4(position.x,position.y,fract(old.b+.03125*u.pass.y),1.);
|
||||
if(u.mask.z>.5){age=initial?particleHash(in.uv+float2(71.19,29.47))*maxAge:old.b+u.pass.w;float2 uv=fieldUv(position,u);float4 a=field.sample(fieldSampler,uv),b=nextField.sample(nextFieldSampler,uv);bool ok=validFieldUv(uv)&&a.a>=.5&&b.a>=.5&&resolveAdvection(a,b,u,advection);respawn=initial||!ok||age>=maxAge||(u.fieldScale.z>.5&&!initial&&(position.x<u.respawnBounds.x||position.x>u.respawnBounds.z||position.y<u.respawnBounds.y||position.y>u.respawnBounds.w));if(!respawn){float lat=mercatorLatitudeRadians(position.y);float2 n=position+float2(advection.x*u.pass.y/(40075016.68557849*max(cos(lat),.01)),-advection.y*u.pass.y/(40075016.68557849*max(cos(lat),.01)));float2 nuv=fieldUv(n,u);float4 na=field.sample(fieldSampler,nuv),nb=nextField.sample(nextFieldSampler,nuv);if(validFieldUv(nuv)&&na.a>=.5&&nb.a>=.5&&resolveAdvection(na,nb,u,advection))position=n;else respawn=true;}if(respawn){bool found=false;for(int i=0;i<3;i++){float2 c=respawnSeed(in.uv,old.rg,float(i),u);float2 cuv=fieldUv(c,u);float4 ca=field.sample(fieldSampler,cuv),cb=nextField.sample(nextFieldSampler,cuv);if(validFieldUv(cuv)&&ca.a>=.5&&cb.a>=.5&&resolveAdvection(ca,cb,u,advection)){position=c;found=true;break;}}if(!found)position=respawnSeed(in.uv,old.rg,3.,u);if(!initial)age=0.;}return float4(position.x,position.y,age,respawn?0.:1.);}
|
||||
float2 uv=fieldUv(position,u);float4 a=field.sample(fieldSampler,uv),b=nextField.sample(nextFieldSampler,uv);if(!validFieldUv(uv)||a.a<.5||b.a<.5||!resolveAdvection(a,b,u,advection)){position=mix(u.fieldBounds.xy,u.fieldBounds.zw,in.uv);advection=float2(0);}float lat=mercatorLatitudeRadians(position.y);position+=float2(advection.x*u.pass.y/(40075016.68557849*max(cos(lat),.01)),-advection.y*u.pass.y/(40075016.68557849*max(cos(lat),.01)));if(!validFieldUv(fieldUv(position,u))||!finite2(advection))position=mix(u.fieldBounds.xy,u.fieldBounds.zw,in.uv);return float4(position.x,position.y,fract(old.b+.03125*u.pass.y),1.);
|
||||
}
|
||||
if(mode<1.5){if(u.mask.z>.5){float f=pow(clamp(u.visual.w,.0001,1.),u.pass.y*u.marker.w);return float4(old.r*f,0,0,1);}float marker=0.;float2 samples[4]={float2(.125),float2(.375),float2(.625),float2(.875)};for(int i=0;i<4;i++)marker=max(marker,1.-smoothstep(0.,u.marker.z,distance(in.uv,state.sample(stateSampler,samples[i]).rg)));return float4(min(old.rgb*u.marker.w+marker*float3(.08,.55,1),float3(1)),1);}
|
||||
if(mode>3.5){if(in.valid<.5)discard_fragment();float speed=length(in.velocity);return float4(1,1,clamp(speed/30.,0.,1.),1);}
|
||||
|
||||
@@ -238,6 +238,9 @@ public:
|
||||
void setForecastPlaybackState(particle::ForecastPlaybackState);
|
||||
bool setForecastPlaybackSpeed(float);
|
||||
bool advanceForecastClock(uint64_t elapsedMs);
|
||||
// Called once per Map update on the owner thread; render layers only read
|
||||
// the resulting immutable projection and never advance this clock.
|
||||
bool tickForecastClock(std::chrono::steady_clock::time_point now);
|
||||
particle::ColorProjectionResult resolveColorProjection() const;
|
||||
particle::ParticleProjectionResult resolveParticleProjection() const;
|
||||
bool clearEncodedField(const std::string& fieldKey);
|
||||
@@ -302,6 +305,10 @@ private:
|
||||
// across clear operations and resolve their own field-keyed windows.
|
||||
particle::EncodedFieldStore encodedFieldStore;
|
||||
particle::ForecastClock forecastClock;
|
||||
std::chrono::steady_clock::time_point lastForecastTick{};
|
||||
// A Source change synchronously invokes Style::onUpdate, which re-enters
|
||||
// Map::onUpdate. Prevent a second forecast tick during the first notification.
|
||||
bool forecastTickInProgress = false;
|
||||
std::optional<particle::ColorBinding> colorBinding;
|
||||
std::optional<particle::ParticleBinding> particleBinding;
|
||||
mapbox::base::WeakPtrFactory<Source> weakFactory{this};
|
||||
|
||||
Reference in New Issue
Block a user