07 — Video as a texture (play from memory)

The whole ladder so far played a clip to the screen. This last step plays it onto something — a spinning 3D cube, with the video as a live texture on every face. The trick that makes it natural is the play-from-memory overload: read the clip’s bytes into an array once, hand them to video_open, and decode-to-texture every frame. No per-frame file IO, no re-reading from disk — load once, reuse forever.

Load once, reference forever

video_open(data, size) takes a borrowed buffer instead of a filename. It references the bytes — it does not copy them — so the buffer has to outlive the player. The idiom is to do everything inside the block that owns the loaded bytes:

    // Read the whole clip into memory ONCE; the player references this buffer for
    // its entire life (the reference-semantics contract of the memory overload), so
    // everything below — open, decode, render — runs inside the load block.
    var rc = 0
    fopen(cfg.video, "rb") $(fh : file) {
        fload(fh, sz) $(data : array<uint8>) {
            rc = run_cube(cfg, data)
        }
    }
    return rc
}

fload hands the block a data : array<uint8> view of the whole file; we open the player against addr(data[0]) and run the entire render loop while that view is still alive. The moment video_open returns, the player is reading straight from your buffer — exactly what you want for an embedded or preloaded clip you render to a texture and replay.

Decode straight into the texture

Each frame is the same borrow you’ve used since tutorial 02 — only now the destination is a GL texture instead of the screen. get_data hands you the RGBA pixels for the length of the block; upload them and let go:

//! Borrow the player's current decoded frame (RGBA) and upload it into the cube
//! texture. `vw`x`vh` is the video size. Call after a successful video_decode.
def public upload_current_frame(player : VideoPlayer?; vw, vh : int) {
    player |> get_data() $(rgb : array<uint8>#) {
        glBindTexture(GL_TEXTURE_2D, texture)
        glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, vw, vh, GL_RGBA, GL_UNSIGNED_BYTE, unsafe(addr(rgb[0])))
    }
}

The cube’s six faces all sample that one texture, so the clip plays on each of them as it turns. Because the player owns the decoded frame and the consumer only borrows it, there is never a copy between the decoder and the GPU.

Self-verifying

The cube is windowed, so the headless gate proves the part that matters in CI: that play-from-memory decodes pixel-identically to play-from-file. Same frame count, same checksum — a true drop-in.

require video
require daslib/fio

struct Decoded {
    frames   : int
    checksum : uint
}

// Decode every frame of an opened player, folding RGBA pixels into a checksum.
def drain(player : VideoPlayer?) : Decoded {
    var d : Decoded
    while (video_decode(player)) {
        d.frames++
        player |> get_data() $(rgb : array<uint8>#) {
            for (b in rgb) {
                d.checksum += uint(b)
            }
        }
    }
    return d
}

def from_file(path : string) : Decoded {
    var p = video_open(path)
    verify(p != null, "open by filename")
    let d = drain(p)
    video_close(p)
    return d
}

def from_memory(path : string) : Decoded {
    var out : Decoded
    let sz = int(stat(path).size)
    verify(sz > 0, "clip exists")
    fopen(path, "rb") $(f : file) {
        fload(f, sz) $(data : array<uint8>) {
            // reference the borrowed bytes (no copy); the buffer outlives the player
            var p = video_open(unsafe(addr(data[0])), length(data))
            verify(p != null, "open from the in-memory buffer")
            out = drain(p)
            video_close(p)
        }
    }
    return out
}

[export]
def main() {
    let clip = "assets/emma/emma_intro.mpg"
    let file = from_file(clip)
    let mem = from_memory(clip)
    verify(file.frames > 0, "the clip decodes to at least one frame")
    verify(mem.frames == file.frames, "play-from-memory decodes the same frame count as play-from-file")
    verify(mem.checksum == file.checksum, "play-from-memory yields pixel-identical frames (reference semantics)")
    print("tutorial 07 ok: {mem.frames} frames from memory, pixel-identical to file (checksum {mem.checksum})\n")
}

Running it

cd <dasVideo>
daslang -load_module . examples/play_texture_cube_gl.das

opens Emma’s intro on a spinning cube, decoded from a single in-memory buffer. Point it at your own clip with --video clip.mpg.

That’s the ladder, end to end: open → decode → borrow → screen → GPU → sound → and now a texture on geometry. The clip never left the buffer you loaded it into.