Rendering live broadcast graphics without a GPU, and what four hours actually cost
Rendering Live Broadcast Graphics without a GPU
Short recap, because this is the second half of a story. In the previous article I described how our on-air graphics at VSporte left vMix. The overlay became a web page fed into the mixer as a single input, which took the graphics out of vMix's memory and made them testable. Then Rive moved the layout work off developers and onto designers. That last step had a consequence nobody planned: a graphic was now a file with a state machine, driven by plain data. It did not need a browser to render. And it had never needed a GPU, we had just always rendered it through tools that came with one attached.
The Path to a GPU-Free Renderer
Can the rendering leave the building? Once the graphic is a Rive file plus a JSON state, the renderer can be anything that runs the Rive state machine and rasterizes vectors. Rive is something you normally render on the web, in a browser or an app, and that is where we ran it. I wanted to know whether the same file could be rendered headless, on a server with no GPU in it at all, and what a live broadcast would cost if it could. Which is the obvious question restated: why is there still a GPU workstation in a rented room attached to every parallel broadcast?
The Journey to a Native Build
The path was not a straight line. First browser screenshots driven by a headless browser, then a WebAssembly renderer on CPU, then a native build, each cheaper and more stable than the last. Before committing to the native build I benchmarked it to establish that thirty frames a second at 1080p is reachable on eight cores, and catalogued what the native path gives up, fonts and automatic layout. That is the sort of thing that decides an engine choice more often than raw throughput does.
The Container Architecture
What I ended up with is a single container. Input is an SRT stream, output is an SRT stream with the graphics burned in. Inside it are four threads.
- StateManager holds a websocket subscription to the graphics state, with an HTTPS polling fallback for when the socket will not establish, and the state itself is plain JSON naming the scene file, the state machine, and the view model as key value pairs, and downloads the Rive file and its assets.
- SrtReceiver decodes incoming video into raw frames.
- RiveRenderer runs the state machine and rasterizes into an RGBA buffer through Skia on CPU.
- Compositor blends the overlay over the frame with premultiplied alpha, encodes with libx264, and pushes the result back out over SRT.
The whole thing is a Docker image whose most important environment variable is the URL it reads its graphics state from.
Choosing the Right Renderer
Skia is the part doing the real work in that third thread and it deserves the credit by name. It is a mature 2D rasterizer, the same engine that draws Chrome's canvas and Android's UI, and vector artwork with a bounded number of shapes is what it is built for. Rive supplies the scene and the state machine, Skia turns it into pixels. Worth saying why Skia and not Rive's own renderer, which is where Rive itself has moved: that renderer is designed around the GPU, and the Skia backend was the mature path to a raster surface on CPU.
Performance and Latency
Worth knowing before you copy the choice: Rive has retired that backend on iOS, Android and WASM. It is still there on Linux, which is where this container lives, but it is a dependency with a direction of travel, and that belongs on the risk list rather than in a footnote. It works on CPU because of what broadcast graphics actually are: vector shapes, text and simple transitions at 1080p. The GPU was never required by the pixels. It was required by the tools that used to draw them.
Topology and Failure
One thing about the topology is worth stating explicitly, because it is the first question anyone from a broadcaster asks and the cloud pitch never answers it. In the topology I built, the clean feed does not go through the container. It runs past it, untouched, straight to the mixer, and what the container receives and burns into is the dirty path. Two consequences fall out of that. The clean feed still exists, which matters to every international partner, regional version and ad insertion downstream, and none of them were ever going to accept a workflow that had only one copy of the program feed with graphics welded into it. And the failure story is smaller than it looks from outside: the container sits inline in one path, the one carrying graphics. If it dies, the mixer still has clean video arriving, and you cut to it. That is a few seconds of a plainer looking broadcast, not a dead one.
Latency Budget
The latency budget, which is where the real work was Anything live has a budget, and the useful question is not "is it low latency" but "where do the milliseconds sit". Ours: SRT input latency. The buffer that lets SRT recover lost packets, a couple hundred milliseconds in practice. The single largest line item, and a deliberate purchase. Push it down and you trade robustness for delay, and on a real network you feel that trade immediately.
Cost Calculation
Getting state to the renderer. State arrives over a websocket, pushed the moment it changes, so this line of the budget is close to free. There is an HTTPS polling fallback for when the socket will not establish, and which one is the default was not an aesthetic choice. The target was under a second from the data changing to the frame that shows it, and a poll interval spends a good part of that budget before the renderer has heard anything at all. Keeping both paths in the same binary is worth it anyway: a fallback you can measure against the default is how you find out what the transport is actually costing you.
Encoder and Queues
Encoder. libx264 tuned for zerolatency with a fast preset. Zerolatency matters more than the preset, because it disables the frame reordering and lookahead that otherwise silently add delay. Queues between the threads. This one bit us. If the compositor falls behind, frames accumulate, and an accumulating queue is latency you cannot see in any config file. We settled on dropping frames deliberately when the queue grows, and logging the drop. A visible dropped frame is a much better failure than a stream drifting steadily further behind live.
Instrumenting the System
A subtler one on the same path: the overlay looked stuttery with nothing overloaded, because frames were arriving unevenly rather than late. Instrument decode, render and composite separately from day one. Aggregate CPU load tells you nothing about this class of problem.
Cost and Scalability
What it costs, and how to check the number One container per broadcast, on a node with no GPU. The bench machine is eight dedicated vCPU and sixteen gigabytes of RAM, which is what 1080p at thirty frames a second and six megabits needs with room to spare. From there the calculation is one line: instance size, times the length of the event, times the hourly price. Two anchors so you can place your own. On the provider I ran it on, four hours land at about a dollar, disk and public address included. On AWS the equivalent box is a c7i.2xlarge, eight vCPU and sixteen gigabytes, and in a local zone it runs about forty six cents an hour on demand, so the same match is about two dollars. Pick your own region and the number moves, which is the point of having two anchors rather than one.
Traffic Costs
One line item to carry across when you do: traffic. That provider bundles traffic into the machine price, so a dollar really is the whole bill. Hyperscalers charge it separately, and six megabits for four hours is roughly eleven gigabytes out, which at usual egress rates adds about another dollar on top of the two. It does not change the argument. Leaving it out of your own spreadsheet will.
The Cost of Cloud Graphics
I am not naming the provider, because the interesting number is not mine, it is yours. Note that the two anchors differ by a factor of two and the argument survives it completely, because what this replaces is a GPU workstation, in a rented room, purchased again for every parallel show. One cost the cloud pitch usually skips, and we paid it. With vMix in the studio the finished picture is right there on the local machine. Move the render out and you have to send it back, over SRT, and on our demo rig that return leg was a machine of its own. Anyone who tells you cloud graphics is purely a subtraction has not run it.
The Shape of the Curve
The property that matters is the shape of the curve, not the absolute number. A parallel broadcast is another container, not another machine and another room. Our demo rig ran two graphics packages over the same source feed into one receiving vMix, and going from two parallel outputs to ten is a config change.
Limitations
Where this is a bad idea I would not pitch this as universal, because it is not. If your graphics are 3D, heavy particle work, or anything that genuinely needs shaders, the CPU rasterizer is the wrong tool and you should stop reading here. This works because broadcast graphics are mostly vectors, text and simple transitions at 1080p. If you need frame accurate integration with studio hardware, SDI in and out, genlock, tally, then a cloud container is in the wrong place on the network no matter how cheap it is.
Conclusion
The economics also assume 1080p at thirty frames a second, and that is worth saying plainly to a European audience, because European broadcast lives at 108p50 and 108p50. Fifty frames is not thirty with a larger number attached to it. Push to 50p, to 4K, or to a high bitrate ladder, and the encode cost per show climbs fast enough that a cheap CPU node stops being cheap. At that point you are buying hardware again, just somewhere else. What it actually takes to hold 108p50 on this architecture is its own article, and I would rather write that one properly than wave at it here.
Comments
No comments yet. Start the discussion.