At the (almost) most basic level a chunk of memory can be used as a framebuffer--the host application would read the pixel data which the WASM bytecode wrote and then write it to the host display via OS-level routines.
There are some plans/experiments at making a framebuffer "device" available as part of WASI.
I've written a couple of graphical WASM host applications that aren't browsers (and which don't use memory for pixel data transfer just integer values returned from a function):
The "WebAssembly Calling Card (WACC) Viewer" is implemented via the Godot game engine and an addon that integrates the Wasmtime WASM runtime with the engine: https://wacc.rancidbacon.com
(Also implemented a WACC Viewer in Rust: https://gitlab.com/RancidBacon/rusty-wacc-viewer)
WACC specifies how to transform three integer values (returned from a function in a WASM module) into a coloured triangle in order to render it on screen.
Another "host application" I implemented was a libretro compatible plugin that loads a WASM module and then feeds the module with input from libretro & retrieves framebuffer pixel data (one pixel at a time :D ) via a WASM function call & writes it to the libretro framebuffer for display.
Corruption that can then be used to subvert the behaviour of functions being called.
WebGL additional validation is so good that the only way to work around exploits is to blacklist user's hardware, OS and GPU drivers. Thus rendering those nice 60 FPS scenes into a single digit FPS caused by software emulation.
Security would be so much better if liability was already thing across the industry, and not only on high integrity systems.
There are proofs of concept using shaders for security attacks.
Or perhaps something like React Native, with WASM standing in for the JS runtime?