1 /*
2 * Copyright 2019 Google LLC
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
8 #include "modules/particles/include/SkParticleEffect.h"
9
10 #include "include/core/SkPaint.h"
11 #include "include/private/SkOnce.h"
12 #include "include/private/SkTPin.h"
13 #include "modules/particles/include/SkParticleBinding.h"
14 #include "modules/particles/include/SkParticleDrawable.h"
15 #include "modules/particles/include/SkReflected.h"
16 #include "modules/skresources/include/SkResources.h"
17 #include "src/core/SkArenaAlloc.h"
18 #include "src/core/SkPaintPriv.h"
19 #include "src/core/SkVM.h"
20 #include "src/sksl/SkSLCompiler.h"
21 #include "src/sksl/SkSLUtil.h"
22 #include "src/sksl/codegen/SkSLVMCodeGenerator.h"
23
24 // Cached state for a single program (either all Effect code, or all Particle code)
25 struct SkParticleProgram {
SkParticleProgramSkParticleProgram26 SkParticleProgram(skvm::Program effectSpawn,
27 skvm::Program effectUpdate,
28 skvm::Program spawn,
29 skvm::Program update,
30 std::vector<std::unique_ptr<SkSL::ExternalFunction>> externalFunctions,
31 skvm::Uniforms externalFunctionUniforms,
32 std::unique_ptr<SkArenaAlloc> alloc,
33 std::unique_ptr<SkSL::UniformInfo> uniformInfo)
34 : fEffectSpawn(std::move(effectSpawn))
35 , fEffectUpdate(std::move(effectUpdate))
36 , fSpawn(std::move(spawn))
37 , fUpdate(std::move(update))
38 , fExternalFunctions(std::move(externalFunctions))
39 , fExternalFunctionUniforms(std::move(externalFunctionUniforms))
40 , fAlloc(std::move(alloc))
41 , fUniformInfo(std::move(uniformInfo)) {}
42
43 // Programs for each entry point
44 skvm::Program fEffectSpawn;
45 skvm::Program fEffectUpdate;
46 skvm::Program fSpawn;
47 skvm::Program fUpdate;
48
49 // External functions created by each SkParticleBinding
50 std::vector<std::unique_ptr<SkSL::ExternalFunction>> fExternalFunctions;
51
52 // Storage for uniforms generated by external functions
53 skvm::Uniforms fExternalFunctionUniforms;
54 std::unique_ptr<SkArenaAlloc> fAlloc;
55
56 // Information about uniforms declared in the SkSL
57 std::unique_ptr<SkSL::UniformInfo> fUniformInfo;
58 };
59
60 static const char* kCommonHeader =
61 R"(
62 struct Effect {
63 float age;
64 float lifetime;
65 int loop;
66 float rate;
67 int burst;
68
69 float2 pos;
70 float2 dir;
71 float scale;
72 float2 vel;
73 float spin;
74 float4 color;
75 float frame;
76 float seed;
77 };
78
79 struct Particle {
80 float age;
81 float lifetime;
82 float2 pos;
83 float2 dir;
84 float scale;
85 float2 vel;
86 float spin;
87 float4 color;
88 float frame;
89 float seed;
90 };
91
92 uniform float dt;
93 uniform Effect effect;
94
95 // We use a not-very-random pure-float PRNG. It does have nice properties for our situation:
96 // It's fast-ish. Importantly, it only uses types and operations that exist in public SkSL's
97 // minimum spec (no bitwise operations on integers).
98 float rand(inout float seed) {
99 seed = sin(31*seed) + sin(19*seed + 1);
100 return fract(abs(10*seed));
101 }
102 )";
103
104 static const char* kDefaultCode =
105 R"(void effectSpawn(inout Effect effect) {
106 }
107
108 void effectUpdate(inout Effect effect) {
109 }
110
111 void spawn(inout Particle p) {
112 }
113
114 void update(inout Particle p) {
115 }
116 )";
117
SkParticleEffectParams()118 SkParticleEffectParams::SkParticleEffectParams()
119 : fMaxCount(128)
120 , fDrawable(nullptr)
121 , fCode(kDefaultCode) {}
122
visitFields(SkFieldVisitor * v)123 void SkParticleEffectParams::visitFields(SkFieldVisitor* v) {
124 v->visit("MaxCount", fMaxCount);
125 v->visit("Drawable", fDrawable);
126 v->visit("Code", fCode);
127 v->visit("Bindings", fBindings);
128 }
129
prepare(const skresources::ResourceProvider * resourceProvider)130 void SkParticleEffectParams::prepare(const skresources::ResourceProvider* resourceProvider) {
131 for (auto& binding : fBindings) {
132 if (binding) {
133 binding->prepare(resourceProvider);
134 }
135 }
136 if (fDrawable) {
137 fDrawable->prepare(resourceProvider);
138 }
139
140 auto buildProgram = [this](const SkSL::String& code) -> std::unique_ptr<SkParticleProgram> {
141 SkSL::ShaderCapsPointer caps = SkSL::ShaderCapsFactory::Standalone();
142 SkSL::Compiler compiler(caps.get());
143
144 // We use two separate blocks of uniforms (ie two args of stride 0). The first is for skvm
145 // uniforms generated by any external functions. These are managed with a Uniforms instance,
146 // and after it's populated, the values never need to be touched again.
147 // The second uniform arg is for things declared as 'uniform' in the SkSL (including the
148 // built-in declarations of 'dt' and 'effect').
149 skvm::Uniforms efUniforms(skvm::UPtr{0}, 0);
150 auto alloc = std::make_unique<SkArenaAlloc>(0);
151
152 std::vector<std::unique_ptr<SkSL::ExternalFunction>> externalFns;
153 externalFns.reserve(fBindings.size());
154 for (const auto& binding : fBindings) {
155 if (binding) {
156 externalFns.push_back(binding->toFunction(compiler, &efUniforms, alloc.get()));
157 }
158 }
159
160 SkSL::Program::Settings settings;
161 settings.fRemoveDeadFunctions = false;
162 settings.fExternalFunctions = &externalFns;
163
164 auto program = compiler.convertProgram(SkSL::ProgramKind::kGeneric, code, settings);
165 if (!program) {
166 SkDebugf("%s\n", compiler.errorText().c_str());
167 return nullptr;
168 }
169
170 std::unique_ptr<SkSL::UniformInfo> uniformInfo = SkSL::Program_GetUniformInfo(*program);
171
172 // For each entry point, convert to an skvm::Program. We need a fresh Builder and uniform
173 // IDs (though we can reuse the Uniforms object, thanks to how it works).
174 auto buildFunction = [&](const char* name){
175 auto fn = SkSL::Program_GetFunction(*program, name);
176 if (!fn) {
177 return skvm::Program{};
178 }
179
180 skvm::Builder b;
181 skvm::UPtr efUniformPtr = b.uniform(), // aka efUniforms.base
182 skslUniformPtr = b.uniform();
183 (void)efUniformPtr;
184
185 std::vector<skvm::Val> uniformIDs;
186 for (int i = 0; i < uniformInfo->fUniformSlotCount; ++i) {
187 uniformIDs.push_back(b.uniform32(skslUniformPtr, i * sizeof(int)).id);
188 }
189 if (!SkSL::ProgramToSkVM(*program, *fn, &b, /*debugInfo=*/nullptr,
190 SkMakeSpan(uniformIDs))) {
191 return skvm::Program{};
192 }
193 return b.done();
194 };
195
196 skvm::Program effectSpawn = buildFunction("effectSpawn"),
197 effectUpdate = buildFunction("effectUpdate"),
198 spawn = buildFunction("spawn"),
199 update = buildFunction("update");
200
201 return std::make_unique<SkParticleProgram>(std::move(effectSpawn),
202 std::move(effectUpdate),
203 std::move(spawn),
204 std::move(update),
205 std::move(externalFns),
206 std::move(efUniforms),
207 std::move(alloc),
208 std::move(uniformInfo));
209 };
210
211 SkSL::String particleCode(kCommonHeader);
212 particleCode.append(fCode.c_str());
213
214 if (auto prog = buildProgram(particleCode)) {
215 fProgram = std::move(prog);
216 }
217 }
218
SkParticleEffect(sk_sp<SkParticleEffectParams> params)219 SkParticleEffect::SkParticleEffect(sk_sp<SkParticleEffectParams> params)
220 : fParams(std::move(params))
221 , fLooping(false)
222 , fCount(0)
223 , fLastTime(-1.0)
224 , fSpawnRemainder(0.0f) {
225 fState.fAge = -1.0f;
226 this->updateStorage();
227 }
228
updateStorage()229 void SkParticleEffect::updateStorage() {
230 // Handle user edits to fMaxCount
231 if (fParams->fMaxCount != fCapacity) {
232 this->setCapacity(fParams->fMaxCount);
233 }
234
235 // Ensure our storage block for uniforms is large enough
236 if (this->uniformInfo()) {
237 int newCount = this->uniformInfo()->fUniformSlotCount;
238 if (newCount > fUniforms.count()) {
239 fUniforms.push_back_n(newCount - fUniforms.count(), 0.0f);
240 } else {
241 fUniforms.resize(newCount);
242 }
243 }
244 }
245
setUniform(const char * name,const float * val,int count)246 bool SkParticleEffect::setUniform(const char* name, const float* val, int count) {
247 const SkSL::UniformInfo* info = this->uniformInfo();
248 if (!info) {
249 return false;
250 }
251
252 auto it = std::find_if(info->fUniforms.begin(), info->fUniforms.end(),
253 [name](const auto& u) { return u.fName == name; });
254 if (it == info->fUniforms.end()) {
255 return false;
256 }
257 if (it->fRows * it->fColumns != count) {
258 return false;
259 }
260
261 std::copy(val, val + count, this->uniformData() + it->fSlot);
262 return true;
263 }
264
start(double now,bool looping,SkPoint position,SkVector heading,float scale,SkVector velocity,float spin,SkColor4f color,float frame,float seed)265 void SkParticleEffect::start(double now, bool looping, SkPoint position, SkVector heading,
266 float scale, SkVector velocity, float spin, SkColor4f color,
267 float frame, float seed) {
268 fCount = 0;
269 fLastTime = now;
270 fSpawnRemainder = 0.0f;
271 fLooping = looping;
272
273 fState.fAge = 0.0f;
274
275 // A default lifetime makes sense - many effects are simple loops that don't really care.
276 // Every effect should define its own rate of emission, or only use bursts, so leave that as
277 // zero initially.
278 fState.fLifetime = 1.0f;
279 fState.fLoopCount = 0;
280 fState.fRate = 0.0f;
281 fState.fBurst = 0;
282
283 fState.fPosition = position;
284 fState.fHeading = heading;
285 fState.fScale = scale;
286 fState.fVelocity = velocity;
287 fState.fSpin = spin;
288 fState.fColor = color;
289 fState.fFrame = frame;
290 fState.fRandom = seed;
291
292 // Defer running effectSpawn until the first update (to reuse the code when looping)
293 }
294
295 // Just the update step from our "rand" function
advance_seed(float x)296 static float advance_seed(float x) {
297 return sinf(31*x) + sinf(19*x + 1);
298 }
299
runEffectScript(EntryPoint entryPoint)300 void SkParticleEffect::runEffectScript(EntryPoint entryPoint) {
301 if (!fParams->fProgram) {
302 return;
303 }
304
305 const skvm::Program& prog = entryPoint == EntryPoint::kSpawn ? fParams->fProgram->fEffectSpawn
306 : fParams->fProgram->fEffectUpdate;
307 if (prog.empty()) {
308 return;
309 }
310
311 constexpr size_t kNumEffectArgs = sizeof(EffectState) / sizeof(int);
312 void* args[kNumEffectArgs
313 + 1 // external function uniforms
314 + 1]; // SkSL uniforms
315
316 args[0] = fParams->fProgram->fExternalFunctionUniforms.buf.data();
317 args[1] = fUniforms.data();
318 for (size_t i = 0; i < kNumEffectArgs; ++i) {
319 args[i + 2] = SkTAddOffset<void>(&fState, i * sizeof(int));
320 }
321
322 memcpy(&fUniforms[1], &fState.fAge, sizeof(EffectState));
323 prog.eval(1, args);
324 }
325
runParticleScript(EntryPoint entryPoint,int start,int count)326 void SkParticleEffect::runParticleScript(EntryPoint entryPoint, int start, int count) {
327 if (!fParams->fProgram) {
328 return;
329 }
330
331 const skvm::Program& prog = entryPoint == EntryPoint::kSpawn ? fParams->fProgram->fSpawn
332 : fParams->fProgram->fUpdate;
333 if (prog.empty()) {
334 return;
335 }
336
337 void* args[SkParticles::kNumChannels
338 + 1 // external function uniforms
339 + 1]; // SkSL uniforms
340 args[0] = fParams->fProgram->fExternalFunctionUniforms.buf.data();
341 args[1] = fUniforms.data();
342 for (int i = 0; i < SkParticles::kNumChannels; ++i) {
343 args[i + 2] = fParticles.fData[i].get() + start;
344 }
345
346 memcpy(&fUniforms[1], &fState.fAge, sizeof(EffectState));
347 prog.eval(count, args);
348 }
349
advanceTime(double now)350 void SkParticleEffect::advanceTime(double now) {
351 // TODO: Sub-frame spawning. Tricky with script driven position. Supply variable effect.age?
352 // Could be done if effect.age were an external value that offset by particle lane, perhaps.
353 float deltaTime = static_cast<float>(now - fLastTime);
354 if (deltaTime <= 0.0f) {
355 return;
356 }
357 fLastTime = now;
358
359 // Possibly re-allocate cached storage, if our params have changed
360 this->updateStorage();
361
362 // Copy known values into the uniform blocks
363 if (fParams->fProgram) {
364 fUniforms[0] = deltaTime;
365 }
366
367 // Is this the first update after calling start()?
368 // Run 'effectSpawn' to set initial emitter properties.
369 if (fState.fAge == 0.0f && fState.fLoopCount == 0) {
370 this->runEffectScript(EntryPoint::kSpawn);
371 }
372
373 fState.fAge += deltaTime / fState.fLifetime;
374 if (fState.fAge > 1) {
375 if (fLooping) {
376 // If we looped, then run effectSpawn again (with the updated loop count)
377 fState.fLoopCount += sk_float_floor2int(fState.fAge);
378 fState.fAge = fmodf(fState.fAge, 1.0f);
379 this->runEffectScript(EntryPoint::kSpawn);
380 } else {
381 // Effect is dead if we've reached the end (and are not looping)
382 return;
383 }
384 }
385
386 // Advance age for existing particles, and remove any that have reached their end of life
387 for (int i = 0; i < fCount; ++i) {
388 fParticles.fData[SkParticles::kAge][i] +=
389 fParticles.fData[SkParticles::kLifetime][i] * deltaTime;
390 if (fParticles.fData[SkParticles::kAge][i] > 1.0f) {
391 // NOTE: This is fast, but doesn't preserve drawing order. Could be a problem...
392 for (int j = 0; j < SkParticles::kNumChannels; ++j) {
393 fParticles.fData[j][i] = fParticles.fData[j][fCount - 1];
394 }
395 fStableRandoms[i] = fStableRandoms[fCount - 1];
396 --i;
397 --fCount;
398 }
399 }
400
401 // Run 'effectUpdate' to adjust emitter properties
402 this->runEffectScript(EntryPoint::kUpdate);
403
404 // Do integration of effect position and orientation
405 {
406 fState.fPosition += fState.fVelocity * deltaTime;
407 float s = sk_float_sin(fState.fSpin * deltaTime),
408 c = sk_float_cos(fState.fSpin * deltaTime);
409 // Using setNormalize to prevent scale drift
410 fState.fHeading.setNormalize(fState.fHeading.fX * c - fState.fHeading.fY * s,
411 fState.fHeading.fX * s + fState.fHeading.fY * c);
412 }
413
414 // Spawn new particles
415 float desired = fState.fRate * deltaTime + fSpawnRemainder + fState.fBurst;
416 fState.fBurst = 0;
417 int numToSpawn = sk_float_round2int(desired);
418 fSpawnRemainder = desired - numToSpawn;
419 numToSpawn = SkTPin(numToSpawn, 0, fParams->fMaxCount - fCount);
420 if (numToSpawn) {
421 const int spawnBase = fCount;
422
423 for (int i = 0; i < numToSpawn; ++i) {
424 // Mutate our random seed so each particle definitely gets a different generator
425 fState.fRandom = advance_seed(fState.fRandom);
426 fParticles.fData[SkParticles::kAge ][fCount] = 0.0f;
427 fParticles.fData[SkParticles::kLifetime ][fCount] = 0.0f;
428 fParticles.fData[SkParticles::kPositionX ][fCount] = fState.fPosition.fX;
429 fParticles.fData[SkParticles::kPositionY ][fCount] = fState.fPosition.fY;
430 fParticles.fData[SkParticles::kHeadingX ][fCount] = fState.fHeading.fX;
431 fParticles.fData[SkParticles::kHeadingY ][fCount] = fState.fHeading.fY;
432 fParticles.fData[SkParticles::kScale ][fCount] = fState.fScale;
433 fParticles.fData[SkParticles::kVelocityX ][fCount] = fState.fVelocity.fX;
434 fParticles.fData[SkParticles::kVelocityY ][fCount] = fState.fVelocity.fY;
435 fParticles.fData[SkParticles::kVelocityAngular][fCount] = fState.fSpin;
436 fParticles.fData[SkParticles::kColorR ][fCount] = fState.fColor.fR;
437 fParticles.fData[SkParticles::kColorG ][fCount] = fState.fColor.fG;
438 fParticles.fData[SkParticles::kColorB ][fCount] = fState.fColor.fB;
439 fParticles.fData[SkParticles::kColorA ][fCount] = fState.fColor.fA;
440 fParticles.fData[SkParticles::kSpriteFrame ][fCount] = fState.fFrame;
441 fParticles.fData[SkParticles::kRandom ][fCount] = fState.fRandom;
442 fCount++;
443 }
444
445 // Run the spawn script
446 this->runParticleScript(EntryPoint::kSpawn, spawnBase, numToSpawn);
447
448 // Now stash copies of the random seeds and compute inverse particle lifetimes
449 // (so that subsequent updates are faster)
450 for (int i = spawnBase; i < fCount; ++i) {
451 fParticles.fData[SkParticles::kLifetime][i] =
452 sk_ieee_float_divide(1.0f, fParticles.fData[SkParticles::kLifetime][i]);
453 fStableRandoms[i] = fParticles.fData[SkParticles::kRandom][i];
454 }
455 }
456
457 // Restore all stable random seeds so update scripts get consistent behavior each frame
458 for (int i = 0; i < fCount; ++i) {
459 fParticles.fData[SkParticles::kRandom][i] = fStableRandoms[i];
460 }
461
462 // Run the update script
463 this->runParticleScript(EntryPoint::kUpdate, 0, fCount);
464
465 // Do fixed-function update work (integration of position and orientation)
466 for (int i = 0; i < fCount; ++i) {
467 fParticles.fData[SkParticles::kPositionX][i] +=
468 fParticles.fData[SkParticles::kVelocityX][i] * deltaTime;
469 fParticles.fData[SkParticles::kPositionY][i] +=
470 fParticles.fData[SkParticles::kVelocityY][i] * deltaTime;
471
472 float spin = fParticles.fData[SkParticles::kVelocityAngular][i];
473 float s = sk_float_sin(spin * deltaTime),
474 c = sk_float_cos(spin * deltaTime);
475 float oldHeadingX = fParticles.fData[SkParticles::kHeadingX][i],
476 oldHeadingY = fParticles.fData[SkParticles::kHeadingY][i];
477 fParticles.fData[SkParticles::kHeadingX][i] = oldHeadingX * c - oldHeadingY * s;
478 fParticles.fData[SkParticles::kHeadingY][i] = oldHeadingX * s + oldHeadingY * c;
479 }
480 }
481
update(double now)482 void SkParticleEffect::update(double now) {
483 if (this->isAlive()) {
484 this->advanceTime(now);
485 }
486 }
487
draw(SkCanvas * canvas)488 void SkParticleEffect::draw(SkCanvas* canvas) {
489 if (this->isAlive() && fParams->fDrawable) {
490 SkPaint paint;
491 fParams->fDrawable->draw(canvas, fParticles, fCount, paint);
492 }
493 }
494
setCapacity(int capacity)495 void SkParticleEffect::setCapacity(int capacity) {
496 for (int i = 0; i < SkParticles::kNumChannels; ++i) {
497 fParticles.fData[i].realloc(capacity);
498 }
499 fStableRandoms.realloc(capacity);
500
501 fCapacity = capacity;
502 fCount = std::min(fCount, fCapacity);
503 }
504
uniformInfo() const505 const SkSL::UniformInfo* SkParticleEffect::uniformInfo() const {
506 return fParams->fProgram ? fParams->fProgram->fUniformInfo.get() : nullptr;
507 }
508
RegisterParticleTypes()509 void SkParticleEffect::RegisterParticleTypes() {
510 static SkOnce once;
511 once([]{
512 REGISTER_REFLECTED(SkReflected);
513 SkParticleBinding::RegisterBindingTypes();
514 SkParticleDrawable::RegisterDrawableTypes();
515 });
516 }
517