ABOUT
Shock Wave Flash — a format from another era
An SWF — originally standing for ShockWave Flash, later rebranded as Small Web Format — is a binary media format developed by Macromedia and later run by Adobe. From the late 1990s through the early 2010s, it was one of the only ways to play interactive content on the web. This included games, animations, entire applications, and multimedia experiences all packed into SWF files.
Calling an SWF a "video file" undersells it considerably. An SWF is a complete compiled program; It contains a timeline, a virtual machine, bytecode, embedded assets, and interactive logic — all packed into a single binary SWF file, while a video file simply plays back a fixed stream of frames. A SWF can branch, respond to input, load external data, and execute code; The two are fundamentally different things.
A SWF file begins with a 3-byte signature — either FWS for uncompressed files, CWS for zlib-compressed (introduced in SWF 6), or ZWS for LZMA-compressed (SWF 13+). Everything after the 8-byte header is a sequence of tagged data blocks called records.
The RECT structure alone illustrates how aggressively SWF packs data. Rather than aligning values to byte boundaries, it stores its four coordinates in variable-width bit fields: the first 5 bits encode the bit-length of each coordinate, and the coordinates follow immediately at that width. Parsing it requires a bit-level reader, not a standard byte stream.
Each tag record consists of a 16-bit field where the upper 10 bits are the tag type and the lower 6 bits are the length — unless the length is 63 or more, in which case a separate 32-bit length field follows. There are over 90 distinct tag types covering everything from shape definitions and bitmaps to font tables, sound streams, and bytecode.
Most binary formats operate on byte-aligned fields. SWF does not. A significant portion of its structures are packed at the bit level, which means a correct parser must maintain a bit cursor independently of the byte cursor at all times. Reading a single coordinate value requires knowing how many bits wide it is — and that width is itself stored in the preceding bits.
To make this concrete: normal byte-aligned packing for RGBA is four bytes, one per channel, trivial to index. Bit-packed RGBA at 2 bits per channel fits in a single byte — but you can no longer index anything. You read bits one at a time, shift them into place, and track exactly where you are in the stream.
SWF shape records mix 4-bit, 5-bit, 6-bit, and variable-width signed fields in the same stream. Coordinate values are stored as signed integers in two's complement at whatever bit-width the preceding header specifies. A 15-bit signed value uses bit 14 as the sign bit — meaning the parser has to sign-extend every value correctly before it means anything in world space.
Flash content is vector-based. Shapes in a SWF are not stored as raster images — they are defined using fill styles, line styles, and edge records that describe curves and lines in twips (twentieths of a point, or 1/1440th of an inch). The renderer recomputes everything at display time, which is why Flash content scaled cleanly to any resolution long before high-DPI displays existed.
Curves use quadratic Bézier segments encoded as straight or curved edge records. Straight edges store delta-X and delta-Y offsets in variable-length bit fields. Curved edges store two control point deltas and an anchor delta, again in bit-packed form. Reconstructing a shape means walking these edge records and accumulating a path — but the path itself has no fixed starting point, no guaranteed ordering, and no requirement that edges arrive in any particular sequence.
The most counterintuitive part of SWF shape rendering is that edges are dual-sided. Every edge record can carry two independent fill style references — one for the region to its left, and one for the region to its right. These are called fillStyle1 and fillStyle0 respectively, and they don't describe the edge itself — they describe what color fills the space on each side of it.
This means a single edge can belong to two different fill regions at the same time. A renderer that simply collects edges per fill style and draws them will get the wrong result — because the same edge drawn in two different directions creates two different contour contributions. The fill on the left side of an edge travelling east is the same as the fill on the right side of the same edge travelling west. Direction matters.
A correct implementation has to split each edge into directed half-edges, accumulate them by fill style index into separate pending paths, and then close and rasterize each path independently. The winding direction of the accumulated segments is what the even-odd fill rule later uses to determine where fills overlap — and where holes should appear.
The even-odd fill rule is how SWF creates shapes with holes — rings, frames, donuts, cutouts of any complexity — without requiring a separate "subtract" operation. The rule is purely geometric: for any point inside a path, cast a ray in any direction and count how many path contours it crosses. If the count is odd, the point is filled. If even, it is not.
For this to work, the outer boundary and the inner hole must be separate closed contours within the same path. The renderer draws both contours into the same path object, and the even-odd rule handles the rest automatically — any area enclosed by exactly one contour is filled, and any area enclosed by two overlapping contours cancels out to transparent.
This is why the winding direction of fillStyle0 edges is reversed — the inner contour needs to wind opposite to the outer one. When both contours are in the same SkPath with the even-odd fill type set, Skia handles the hole punching correctly. Getting the winding wrong in either direction produces a filled circle instead of a ring, or a ring filled in the wrong region.
SWF rendering is not frame-independent. Unlike a video codec that encodes every frame in isolation, an SWF player maintains a persistent display list that accumulates state across the entire timeline. Shapes are defined once in the character dictionary and then placed, moved, and removed from the display list using control tags.
Every object on the display list has a depth — a positive integer that determines draw order. Higher depths render on top of lower ones. A PlaceObject tag can add a new character at a given depth, replace whatever was there before, or modify an existing entry's transform or color. A RemoveObject tag removes an entry from the list entirely.
At every ShowFrame tag, the renderer must flush the current display list to the screen in depth order — drawing the background clear, then each depth from lowest to highest. None of this is implicit; the player has to maintain the full list across every frame and correctly handle characters that are defined after they are first placed, characters that update their transform without changing their ID, and characters that are removed and re-added at the same depth across frames.
And underneath all of that, later SWF versions embed ActionScript bytecode — first AVM1 (a stack-based virtual machine for AS1/AS2), and later AVM2 (a register-based VM for AS3 with typed operands, exception handling, and a full class system). Executing that correctly is a separate and substantial problem, which is why full SWF support remains an ongoing engineering effort rather than a solved one.
WasFlash runs a WebAssembly build of a native SWF renderer directly in your browser. The WASM binary handles all parsing, display list management, and vector rasterization. The JavaScript layer sets the required cross-origin headers (COOP/COEP) to enable SharedArrayBuffer, which allows the renderer to use POSIX threads via Emscripten's pthread support — the same threading model it would have on a native system.
The result is a zero-install, zero-plugin SWF player that runs entirely client-side. The hope is that one day you will be able to just embed this into your website, and it will play whatever content you please.