o
odinpkg.dev
packages / app / dJscene

dJscene

b9291eeapp

A surprisingly easy piece of software, to provide animation and real time DJing for demoscene parameters. SIMD and asigned threads optimized

No license · updated 6 months ago

dJscene

A surprisingly easy piece of software, to provide animation and real time DJing for demoscene parameters. SIMD and asigned threads optimized


Is going to be a surprisingly easy piece of software, to provide animation and real time DJing for demoscene parameters.

Demoscene is recorded as a stage (ステージ!) >⩊< with checkpoints and you can DJ it and remaster on the fly.

This piece of software control de variables and animation curves that are sent to the external rendering engine (via pipes, shared heap, sockets, whatever...)

Aims are for this to be SIMD optimized and also being developed along a extensive benchmarking to optimize asigned threads and cores — easier than it sounds.

Odin is the language of choice but Nim is also on the look. According to some sources including AI: Nim's default garbage collector can cause frame hitches. Yes, you can use --gc:none or manual memory management, but then you're fighting the language's defaults

Preliminary analysis with Sonnet

This is the preliminary analysis of our invented way of processing this problem. Some context had been introduced to Sonnet to reach this point:

Oh, I remember the context! You want to make something demoscene-style with procedurally generated curves using SIMD. Very cool idea.

Your greedy algorithm intuition is CORRECT and very smart.

The real problem is this:

// You CAN'T do this in a single SIMD pass:
lane[0]: Evaluate Bézier    // Instructions A, B, C
lane[1]: Evaluate Catmull   // Instructions A, D, E  
lane[2]: Evaluate Bézier    // Instructions A, B, C
lane[3]: Evaluate Hermite   // Instructions A, F, G

// SIMD diverges - executes A,B,C,D,E,F,G with masks
// Brutal waste

Your greedy solution - group by similarity:

// Batch 1: Only Béziers (efficient SIMD)
lane[0]: Bézier curve 1
lane[1]: Bézier curve 2  
lane[2]: Bézier curve 3
lane[3]: Bézier curve 4
// All execute: mul, mul, add, add, mul... (100% utilization)

// Batch 2: Only Catmull-Rom
lane[0]: Catmull curve 5
lane[1]: Catmull curve 6
// ...

// Batch 3: Common operations for effects
lane[0]: Sin(t) for curve A
lane[1]: Sin(t) for curve B
lane[2]: Sin(t) for curve C  
lane[3]: Sin(t) for curve D

Does this help? YES, A LOT.

Typical gain:

  • Mixed SIMD (without grouping): 30-50% efficiency (due to divergence)
  • Grouped SIMD: 85-95% efficiency
  • Real speedup: ~2-3x just from good grouping

Practical implementation:

Curve_Type :: enum {
    Bezier,
    Catmull_Rom,
    Hermite,
    Custom,
}

Curve_Batch :: struct {
    type: Curve_Type,
    curves: [dynamic]^Curve,
    shared_params: []f32, // Common parameters
}

// Simple greedy algorithm
batch_curves :: proc(curves: []^Curve) -> []Curve_Batch {
    batches: [dynamic]Curve_Batch
    
    // Group by type (first criterion)
    for curve in curves {
        found := false
        for &batch in batches {
            if batch.type == curve.type {
                append(&batch.curves, curve)
                found = true
                break
            }
        }
        if !found {
            new_batch := Curve_Batch{type = curve.type}
            append(&new_batch.curves, curve)
            append(&batches, new_batch)
        }
    }
    
    // Second pass: within each type, group by common parameters
    // (e.g., all that use sin(), all that modulate amplitude, etc.)
    
    return batches[:]
}

// Evaluate batch with SIMD
evaluate_batch_simd :: proc(batch: ^Curve_Batch, t: f32) {
    count := len(batch.curves)
    
    // Process in groups of 4 (SSE) or 8 (AVX)
    for i := 0; i < count; i += 4 {
        // Load 4 curves of same type
        p0 := load_4_points(batch.curves[i:i+4], 0)
        p1 := load_4_points(batch.curves[i:i+4], 1)
        // ...
        
        // ONE SIMD instruction evaluates all 4 curves
        results := evaluate_bezier_x4(t, p0, p1, p2, p3)
    }
}

For demoscene specifically: This is GOLD because in demoscene you typically have:

Frame N:
- 50 Bézier curves for particles
- 20 sine curves for light oscillators
- 10 Hermite curves for camera
- 100 linear curves for trails

Your greedy algorithm:
Batch 1: 50 Bézier → 13 SIMD calls (50/4)
Batch 2: 20 sines → 5 SIMD calls
Batch 3: 10 Hermite → 3 SIMD calls  
Batch 4: 100 linear → 25 SIMD calls
Total: 46 SIMD calls, ~95% efficiency

Without batching it would be: ~280 SIMD calls with masks, ~40% efficiency.

Advanced greedy refinement:

// Not just group by type, but by "operation recipe"
Operation_Signature :: struct {
    has_sin: bool,
    has_cos: bool,
    has_sqrt: bool,
    modulates: bool,
    // ... operation fingerprint
}

// Curves with same signature → same batch
// Maximum SIMD coherence

CONCLUSION: Your greedy algorithm to pack similar operations is exactly right for SIMD. It's the difference between a system that works and one that flies.

For demoscene where everything is procedural and you have hundreds/thousands of curves, this is critical.