fix: finalize GPU particle time interpolation
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
OpenAI Codex
2026-09-02 22:07:01 +08:00
parent d4c17fc8b0
commit 5dd4dbf496
11 changed files with 86 additions and 16 deletions

View File

@@ -193,7 +193,8 @@ ParticleFieldSnapshot OfflineFixtureParticleAdapter::load(const std::string& fix
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()) {
field.fieldSha256 = member(integrity, "fieldSha256").GetString();
if (sha256(bytes) != field.fieldSha256) {
throw std::invalid_argument("fixture field checksum mismatch: " + fixtureID);
}
field.velocity.resize(expected / sizeof(float));

View File

@@ -21,6 +21,9 @@ struct ParticleFieldSnapshot {
float noData = -9999.0f;
uint64_t dataVersion = 0;
uint64_t generation = 0;
// Verified integrity.fieldSha256 identifies immutable GPU-upload content
// across source reloads; generation remains a separate runtime revision.
std::string fieldSha256;
// 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;
@@ -41,6 +44,11 @@ inline bool fieldIdentityChanged(const ParticleFieldSnapshot& current, const Par
current.dataVersion != next.dataVersion;
}
inline bool sameFieldGpuContent(const ParticleFieldSnapshot& current, const ParticleFieldSnapshot& next) {
return current.fixtureID == next.fixtureID && current.dataVersion == next.dataVersion &&
current.fieldSha256 == next.fieldSha256 && current.isTimeInterpolationCompatibleWith(next);
}
// A formal source time window. T0/T1 remain independent immutable snapshots;
// the renderer uploads and samples them independently on the GPU.
struct ParticleFieldTimeWindow {

View File

@@ -732,10 +732,10 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
Log::Error(Event::Render, "ParticleGPU: rejected incompatible time-window fields");
return;
}
const bool reuseOldT0 = !particle::fieldIdentityChanged(oldT0, nextT0);
const bool reuseOldT1 = !particle::fieldIdentityChanged(oldT1, nextT0);
const bool reuseNewT0 = !particle::fieldIdentityChanged(oldT0, nextT1);
const bool reuseNewT1 = !particle::fieldIdentityChanged(oldT1, nextT1);
const bool reuseOldT0 = particle::sameFieldGpuContent(oldT0, nextT0);
const bool reuseOldT1 = particle::sameFieldGpuContent(oldT1, nextT0);
const bool reuseNewT0 = particle::sameFieldGpuContent(oldT0, nextT1);
const bool reuseNewT1 = particle::sameFieldGpuContent(oldT1, nextT1);
const bool windowOverlap = reuseOldT0 || reuseOldT1 || reuseNewT0 || reuseNewT1;
const auto upload = [&](const particle::ParticleFieldSnapshot& field,
std::shared_ptr<PremultipliedImage>& image,
@@ -754,13 +754,13 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
particle::ParticleFieldSnapshot& assigned,
std::shared_ptr<PremultipliedImage>& image,
gfx::Texture2DPtr& texture) {
if (!particle::fieldIdentityChanged(oldT0, wanted)) {
if (particle::sameFieldGpuContent(oldT0, wanted)) {
assigned = oldT0;
image = resources->fieldImage;
texture = resources->fieldTexture;
return true;
}
if (!particle::fieldIdentityChanged(oldT1, wanted)) {
if (particle::sameFieldGpuContent(oldT1, wanted)) {
assigned = oldT1;
image = resources->nextFieldImage;
texture = resources->nextFieldTexture;