There's something uniquely captivating about revisiting the passions of our past. I still remember the countless hours spent tinkering with code on my old 386 and 486 PCs, mesmerized by the magic of pixels coming to life on the screen. The simplicity of those days, where an array of pixels and a spark of imagination were all you needed, still holds a special place in my heart. Inspired by that nostalgia, I decided to breathe new life into one of my early projects - a 2D firework simulation - using modern tools and techniques. I am using Mode-13HX as a basis today, for more details please check my previous part: Old-School Graphics in C# / .Net 8, Part 1.
History
This was originally written in (or before) a year 2000 using Pascal (programming language) and compiled using Turbo Pascal. It used video mode 13h under MS-DOS. Originally 320x200 pixels @ 8bit palette / 256 colors. I have ported this project multiple times in last 20+ years, since it is a small and fun project to play with.
I am running it at 4k today, thanks to modern C# and Mode-13HX. It should work on any platform and architecture where .NET 8 and C# is supported. I added special code path for AVX512 (fading) and SSE (color mixing) to speed things up - If You are interested in these, please keep reading, scroll right to code samples or check the GitHub repo right away.
Fun with Particles
The core of the simulation revolves around a 2D particle system that emulates fireworks bursting in the night sky. Each firework is composed of multiple particles that are generated, animated, and eventually discarded.
- Initial Launch: Particles are spawned with initial positions, velocities and countdown timer that simulate the upward launch of a firework rocket.
- Explosion Stages: Each particle has a timer determining its lifespan, ensuring that old particles are removed to make way for new ones. When time is out, particles could burst into additional particles (or disappear), creating multiple stages of explosion for a more dynamic display.
- Gravity and Air: Somewhat realistic motion is achieved by applying gravitational acceleration and air resistance, affecting each particle's trajectory over time. Original simulation expected constant framerate and particle deceleration was expressed as a relative speed loss over constant time period. For example 85% of speed after 1/60s. In order to account for the variable (or unknown constant) frame rate, I recalculated this into a look-up table (velocity and a position decay factors) for all possible frame offsets with a microsecond resolution. Maximum frame offset is capped, so we don't run out of pre-calculated table.
To create a vibrant and immersive visual experience, the simulation uses additive color blending.
- Additive Blending: When particles overlap, their colors are added together, increasing brightness and creating a glow effect.
- Flare Rendering: Each particle is drawn not just as a single point but with a small flare (1+4 pixels) to enhance the luminosity and visual appeal.
- Color Sets: Multiple predefined color sets (e.g.: standard RGB, pastel tones) are used to diversify the fireworks' appearance.
A critical aspect of the simulation is the gradual fading of framebuffer to simulate the dissipating light of fireworks.
- Frame Buffer Fading: The entire frame buffer undergoes a fading process where pixel brightness decreases over time. This is actually quite inefficient approach because we need to swap at least 2 back buffers. This was not the case in DOS days, but this time the particle drawing and fading is done using a separate (+1 additional) buffer.
Performance Optimizations with Vectorization
To maintain high performance, especially when rendering thousands of particles, the simulation employs vectorization techniques using SIMD (Single Instruction, Multiple Data) instructions available in modern CPUs.
While it may look obvious to use vectorization to accelerate the particle movement itself, I opted not to do so. In terms of CPU usage, the most expensive parts are color blending, fading and page flipping (copying of a framebuffer into texture buffer). It might be an interesting exercise to rewrite this whole thing into OpenCL though. But, Hey! Maybe here is the place for a link to Your repository!
SSE Optimization for Color Mixing. When blending colors for overlapping particles, SSE instructions add multiple color components (red, green, blue - 8bits each) in parallel. One funny (or sad) fact: only 24bits out of 128bit vector are used effectively, but it is still worth the performance increase!
private static void MixColors(ref uint target, uint color)
{
if (Ssse3.IsSupported)
{
Vector128<uint> targetVector = Vector128.CreateScalar(target);
Vector128<uint> colorVector = Vector128.CreateScalar(color);
Vector128<byte> targetBytes = targetVector.AsByte();
Vector128<byte> colorBytes = colorVector.AsByte();
Vector128<byte> resultBytes = Sse2.AddSaturate(targetBytes, colorBytes);
target = resultBytes.AsUInt32().ToScalar();
}
else
{
// Fallback to non-SIMD code
}
}
Frame Buffer Fading: The fading effect subtracts a small value from each color channel across all pixels. AVX-512 accelerates this by handling multiple (16) pixels simultaneously. This reduces the CPU load and ensures that the fading effect doesn't become a performance bottleneck, even at high resolutions.
private const int VectorSize = 64;
private void FadeScenePixels(uint[] pixels, double timePassed)
{
if (Avx512BW.IsSupported)
{
int totalBytes = pixels.Length * sizeof(uint);
int fadeValue = CalculateFadeValue(timePassed);
Vector512<byte> fadeVector = Vector512.Create((byte)fadeValue);
unsafe
{
fixed (uint* pScenePixel = pixels)
{
byte* p = (byte*)pScenePixel;
for (int i = 0; i + VectorSize <= totalBytes; i += VectorSize)
{
Vector512<byte> pixelVector = Avx512BW.LoadVector512(p + i);
pixelVector = Avx512BW.SubtractSaturate(pixelVector, fadeVector);
Avx512BW.Store(p + i, pixelVector);
}
}
}
}
else
{
// Fallback to non-SIMD code
}
}
While it would be possible to continue with more optimizations and enhancements, I believe this might be a good stopping point for now. I like to keep fun projects small and simple, so they keep being funny.
Source code: Fireworks 2D GitHub repository.
Top comments (0)