/* * Copyright 2019 Google LLC * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "modules/particles/include/SkParticleEffect.h" #include "include/core/SkPaint.h" #include "include/private/SkOnce.h" #include "include/private/SkTPin.h" #include "modules/particles/include/SkParticleBinding.h" #include "modules/particles/include/SkParticleDrawable.h" #include "modules/particles/include/SkReflected.h" #include "modules/skresources/include/SkResources.h" #include "src/core/SkArenaAlloc.h" #include "src/core/SkPaintPriv.h" #include "src/core/SkVM.h" #include "src/sksl/SkSLCompiler.h" #include "src/sksl/SkSLUtil.h" #include "src/sksl/codegen/SkSLVMCodeGenerator.h" // Cached state for a single program (either all Effect code, or all Particle code) struct SkParticleProgram { SkParticleProgram(skvm::Program effectSpawn, skvm::Program effectUpdate, skvm::Program spawn, skvm::Program update, std::vector> externalFunctions, skvm::Uniforms externalFunctionUniforms, std::unique_ptr alloc, std::unique_ptr uniformInfo) : fEffectSpawn(std::move(effectSpawn)) , fEffectUpdate(std::move(effectUpdate)) , fSpawn(std::move(spawn)) , fUpdate(std::move(update)) , fExternalFunctions(std::move(externalFunctions)) , fExternalFunctionUniforms(std::move(externalFunctionUniforms)) , fAlloc(std::move(alloc)) , fUniformInfo(std::move(uniformInfo)) {} // Programs for each entry point skvm::Program fEffectSpawn; skvm::Program fEffectUpdate; skvm::Program fSpawn; skvm::Program fUpdate; // External functions created by each SkParticleBinding std::vector> fExternalFunctions; // Storage for uniforms generated by external functions skvm::Uniforms fExternalFunctionUniforms; std::unique_ptr fAlloc; // Information about uniforms declared in the SkSL std::unique_ptr fUniformInfo; }; static const char* kCommonHeader = R"( struct Effect { float age; float lifetime; int loop; float rate; int burst; float2 pos; float2 dir; float scale; float2 vel; float spin; float4 color; float frame; float seed; }; struct Particle { float age; float lifetime; float2 pos; float2 dir; float scale; float2 vel; float spin; float4 color; float frame; float seed; }; uniform float dt; uniform Effect effect; // We use a not-very-random pure-float PRNG. It does have nice properties for our situation: // It's fast-ish. Importantly, it only uses types and operations that exist in public SkSL's // minimum spec (no bitwise operations on integers). float rand(inout float seed) { seed = sin(31*seed) + sin(19*seed + 1); return fract(abs(10*seed)); } )"; static const char* kDefaultCode = R"(void effectSpawn(inout Effect effect) { } void effectUpdate(inout Effect effect) { } void spawn(inout Particle p) { } void update(inout Particle p) { } )"; SkParticleEffectParams::SkParticleEffectParams() : fMaxCount(128) , fDrawable(nullptr) , fCode(kDefaultCode) {} void SkParticleEffectParams::visitFields(SkFieldVisitor* v) { v->visit("MaxCount", fMaxCount); v->visit("Drawable", fDrawable); v->visit("Code", fCode); v->visit("Bindings", fBindings); } void SkParticleEffectParams::prepare(const skresources::ResourceProvider* resourceProvider) { for (auto& binding : fBindings) { if (binding) { binding->prepare(resourceProvider); } } if (fDrawable) { fDrawable->prepare(resourceProvider); } auto buildProgram = [this](const SkSL::String& code) -> std::unique_ptr { SkSL::ShaderCapsPointer caps = SkSL::ShaderCapsFactory::Standalone(); SkSL::Compiler compiler(caps.get()); // We use two separate blocks of uniforms (ie two args of stride 0). The first is for skvm // uniforms generated by any external functions. These are managed with a Uniforms instance, // and after it's populated, the values never need to be touched again. // The second uniform arg is for things declared as 'uniform' in the SkSL (including the // built-in declarations of 'dt' and 'effect'). skvm::Uniforms efUniforms(skvm::UPtr{0}, 0); auto alloc = std::make_unique(0); std::vector> externalFns; externalFns.reserve(fBindings.size()); for (const auto& binding : fBindings) { if (binding) { externalFns.push_back(binding->toFunction(compiler, &efUniforms, alloc.get())); } } SkSL::Program::Settings settings; settings.fRemoveDeadFunctions = false; settings.fExternalFunctions = &externalFns; auto program = compiler.convertProgram(SkSL::ProgramKind::kGeneric, code, settings); if (!program) { SkDebugf("%s\n", compiler.errorText().c_str()); return nullptr; } std::unique_ptr uniformInfo = SkSL::Program_GetUniformInfo(*program); // For each entry point, convert to an skvm::Program. We need a fresh Builder and uniform // IDs (though we can reuse the Uniforms object, thanks to how it works). auto buildFunction = [&](const char* name){ auto fn = SkSL::Program_GetFunction(*program, name); if (!fn) { return skvm::Program{}; } skvm::Builder b; skvm::UPtr efUniformPtr = b.uniform(), // aka efUniforms.base skslUniformPtr = b.uniform(); (void)efUniformPtr; std::vector uniformIDs; for (int i = 0; i < uniformInfo->fUniformSlotCount; ++i) { uniformIDs.push_back(b.uniform32(skslUniformPtr, i * sizeof(int)).id); } if (!SkSL::ProgramToSkVM(*program, *fn, &b, /*debugInfo=*/nullptr, SkMakeSpan(uniformIDs))) { return skvm::Program{}; } return b.done(); }; skvm::Program effectSpawn = buildFunction("effectSpawn"), effectUpdate = buildFunction("effectUpdate"), spawn = buildFunction("spawn"), update = buildFunction("update"); return std::make_unique(std::move(effectSpawn), std::move(effectUpdate), std::move(spawn), std::move(update), std::move(externalFns), std::move(efUniforms), std::move(alloc), std::move(uniformInfo)); }; SkSL::String particleCode(kCommonHeader); particleCode.append(fCode.c_str()); if (auto prog = buildProgram(particleCode)) { fProgram = std::move(prog); } } SkParticleEffect::SkParticleEffect(sk_sp params) : fParams(std::move(params)) , fLooping(false) , fCount(0) , fLastTime(-1.0) , fSpawnRemainder(0.0f) { fState.fAge = -1.0f; this->updateStorage(); } void SkParticleEffect::updateStorage() { // Handle user edits to fMaxCount if (fParams->fMaxCount != fCapacity) { this->setCapacity(fParams->fMaxCount); } // Ensure our storage block for uniforms is large enough if (this->uniformInfo()) { int newCount = this->uniformInfo()->fUniformSlotCount; if (newCount > fUniforms.count()) { fUniforms.push_back_n(newCount - fUniforms.count(), 0.0f); } else { fUniforms.resize(newCount); } } } bool SkParticleEffect::setUniform(const char* name, const float* val, int count) { const SkSL::UniformInfo* info = this->uniformInfo(); if (!info) { return false; } auto it = std::find_if(info->fUniforms.begin(), info->fUniforms.end(), [name](const auto& u) { return u.fName == name; }); if (it == info->fUniforms.end()) { return false; } if (it->fRows * it->fColumns != count) { return false; } std::copy(val, val + count, this->uniformData() + it->fSlot); return true; } void SkParticleEffect::start(double now, bool looping, SkPoint position, SkVector heading, float scale, SkVector velocity, float spin, SkColor4f color, float frame, float seed) { fCount = 0; fLastTime = now; fSpawnRemainder = 0.0f; fLooping = looping; fState.fAge = 0.0f; // A default lifetime makes sense - many effects are simple loops that don't really care. // Every effect should define its own rate of emission, or only use bursts, so leave that as // zero initially. fState.fLifetime = 1.0f; fState.fLoopCount = 0; fState.fRate = 0.0f; fState.fBurst = 0; fState.fPosition = position; fState.fHeading = heading; fState.fScale = scale; fState.fVelocity = velocity; fState.fSpin = spin; fState.fColor = color; fState.fFrame = frame; fState.fRandom = seed; // Defer running effectSpawn until the first update (to reuse the code when looping) } // Just the update step from our "rand" function static float advance_seed(float x) { return sinf(31*x) + sinf(19*x + 1); } void SkParticleEffect::runEffectScript(EntryPoint entryPoint) { if (!fParams->fProgram) { return; } const skvm::Program& prog = entryPoint == EntryPoint::kSpawn ? fParams->fProgram->fEffectSpawn : fParams->fProgram->fEffectUpdate; if (prog.empty()) { return; } constexpr size_t kNumEffectArgs = sizeof(EffectState) / sizeof(int); void* args[kNumEffectArgs + 1 // external function uniforms + 1]; // SkSL uniforms args[0] = fParams->fProgram->fExternalFunctionUniforms.buf.data(); args[1] = fUniforms.data(); for (size_t i = 0; i < kNumEffectArgs; ++i) { args[i + 2] = SkTAddOffset(&fState, i * sizeof(int)); } memcpy(&fUniforms[1], &fState.fAge, sizeof(EffectState)); prog.eval(1, args); } void SkParticleEffect::runParticleScript(EntryPoint entryPoint, int start, int count) { if (!fParams->fProgram) { return; } const skvm::Program& prog = entryPoint == EntryPoint::kSpawn ? fParams->fProgram->fSpawn : fParams->fProgram->fUpdate; if (prog.empty()) { return; } void* args[SkParticles::kNumChannels + 1 // external function uniforms + 1]; // SkSL uniforms args[0] = fParams->fProgram->fExternalFunctionUniforms.buf.data(); args[1] = fUniforms.data(); for (int i = 0; i < SkParticles::kNumChannels; ++i) { args[i + 2] = fParticles.fData[i].get() + start; } memcpy(&fUniforms[1], &fState.fAge, sizeof(EffectState)); prog.eval(count, args); } void SkParticleEffect::advanceTime(double now) { // TODO: Sub-frame spawning. Tricky with script driven position. Supply variable effect.age? // Could be done if effect.age were an external value that offset by particle lane, perhaps. float deltaTime = static_cast(now - fLastTime); if (deltaTime <= 0.0f) { return; } fLastTime = now; // Possibly re-allocate cached storage, if our params have changed this->updateStorage(); // Copy known values into the uniform blocks if (fParams->fProgram) { fUniforms[0] = deltaTime; } // Is this the first update after calling start()? // Run 'effectSpawn' to set initial emitter properties. if (fState.fAge == 0.0f && fState.fLoopCount == 0) { this->runEffectScript(EntryPoint::kSpawn); } fState.fAge += deltaTime / fState.fLifetime; if (fState.fAge > 1) { if (fLooping) { // If we looped, then run effectSpawn again (with the updated loop count) fState.fLoopCount += sk_float_floor2int(fState.fAge); fState.fAge = fmodf(fState.fAge, 1.0f); this->runEffectScript(EntryPoint::kSpawn); } else { // Effect is dead if we've reached the end (and are not looping) return; } } // Advance age for existing particles, and remove any that have reached their end of life for (int i = 0; i < fCount; ++i) { fParticles.fData[SkParticles::kAge][i] += fParticles.fData[SkParticles::kLifetime][i] * deltaTime; if (fParticles.fData[SkParticles::kAge][i] > 1.0f) { // NOTE: This is fast, but doesn't preserve drawing order. Could be a problem... for (int j = 0; j < SkParticles::kNumChannels; ++j) { fParticles.fData[j][i] = fParticles.fData[j][fCount - 1]; } fStableRandoms[i] = fStableRandoms[fCount - 1]; --i; --fCount; } } // Run 'effectUpdate' to adjust emitter properties this->runEffectScript(EntryPoint::kUpdate); // Do integration of effect position and orientation { fState.fPosition += fState.fVelocity * deltaTime; float s = sk_float_sin(fState.fSpin * deltaTime), c = sk_float_cos(fState.fSpin * deltaTime); // Using setNormalize to prevent scale drift fState.fHeading.setNormalize(fState.fHeading.fX * c - fState.fHeading.fY * s, fState.fHeading.fX * s + fState.fHeading.fY * c); } // Spawn new particles float desired = fState.fRate * deltaTime + fSpawnRemainder + fState.fBurst; fState.fBurst = 0; int numToSpawn = sk_float_round2int(desired); fSpawnRemainder = desired - numToSpawn; numToSpawn = SkTPin(numToSpawn, 0, fParams->fMaxCount - fCount); if (numToSpawn) { const int spawnBase = fCount; for (int i = 0; i < numToSpawn; ++i) { // Mutate our random seed so each particle definitely gets a different generator fState.fRandom = advance_seed(fState.fRandom); fParticles.fData[SkParticles::kAge ][fCount] = 0.0f; fParticles.fData[SkParticles::kLifetime ][fCount] = 0.0f; fParticles.fData[SkParticles::kPositionX ][fCount] = fState.fPosition.fX; fParticles.fData[SkParticles::kPositionY ][fCount] = fState.fPosition.fY; fParticles.fData[SkParticles::kHeadingX ][fCount] = fState.fHeading.fX; fParticles.fData[SkParticles::kHeadingY ][fCount] = fState.fHeading.fY; fParticles.fData[SkParticles::kScale ][fCount] = fState.fScale; fParticles.fData[SkParticles::kVelocityX ][fCount] = fState.fVelocity.fX; fParticles.fData[SkParticles::kVelocityY ][fCount] = fState.fVelocity.fY; fParticles.fData[SkParticles::kVelocityAngular][fCount] = fState.fSpin; fParticles.fData[SkParticles::kColorR ][fCount] = fState.fColor.fR; fParticles.fData[SkParticles::kColorG ][fCount] = fState.fColor.fG; fParticles.fData[SkParticles::kColorB ][fCount] = fState.fColor.fB; fParticles.fData[SkParticles::kColorA ][fCount] = fState.fColor.fA; fParticles.fData[SkParticles::kSpriteFrame ][fCount] = fState.fFrame; fParticles.fData[SkParticles::kRandom ][fCount] = fState.fRandom; fCount++; } // Run the spawn script this->runParticleScript(EntryPoint::kSpawn, spawnBase, numToSpawn); // Now stash copies of the random seeds and compute inverse particle lifetimes // (so that subsequent updates are faster) for (int i = spawnBase; i < fCount; ++i) { fParticles.fData[SkParticles::kLifetime][i] = sk_ieee_float_divide(1.0f, fParticles.fData[SkParticles::kLifetime][i]); fStableRandoms[i] = fParticles.fData[SkParticles::kRandom][i]; } } // Restore all stable random seeds so update scripts get consistent behavior each frame for (int i = 0; i < fCount; ++i) { fParticles.fData[SkParticles::kRandom][i] = fStableRandoms[i]; } // Run the update script this->runParticleScript(EntryPoint::kUpdate, 0, fCount); // Do fixed-function update work (integration of position and orientation) for (int i = 0; i < fCount; ++i) { fParticles.fData[SkParticles::kPositionX][i] += fParticles.fData[SkParticles::kVelocityX][i] * deltaTime; fParticles.fData[SkParticles::kPositionY][i] += fParticles.fData[SkParticles::kVelocityY][i] * deltaTime; float spin = fParticles.fData[SkParticles::kVelocityAngular][i]; float s = sk_float_sin(spin * deltaTime), c = sk_float_cos(spin * deltaTime); float oldHeadingX = fParticles.fData[SkParticles::kHeadingX][i], oldHeadingY = fParticles.fData[SkParticles::kHeadingY][i]; fParticles.fData[SkParticles::kHeadingX][i] = oldHeadingX * c - oldHeadingY * s; fParticles.fData[SkParticles::kHeadingY][i] = oldHeadingX * s + oldHeadingY * c; } } void SkParticleEffect::update(double now) { if (this->isAlive()) { this->advanceTime(now); } } void SkParticleEffect::draw(SkCanvas* canvas) { if (this->isAlive() && fParams->fDrawable) { SkPaint paint; fParams->fDrawable->draw(canvas, fParticles, fCount, paint); } } void SkParticleEffect::setCapacity(int capacity) { for (int i = 0; i < SkParticles::kNumChannels; ++i) { fParticles.fData[i].realloc(capacity); } fStableRandoms.realloc(capacity); fCapacity = capacity; fCount = std::min(fCount, fCapacity); } const SkSL::UniformInfo* SkParticleEffect::uniformInfo() const { return fParams->fProgram ? fParams->fProgram->fUniformInfo.get() : nullptr; } void SkParticleEffect::RegisterParticleTypes() { static SkOnce once; once([]{ REGISTER_REFLECTED(SkReflected); SkParticleBinding::RegisterBindingTypes(); SkParticleDrawable::RegisterDrawableTypes(); }); }