use wasm_bindgen::{JsCast, JsValue};
use web_sys::{
WebGl2RenderingContext as Gl, WebGlBuffer, WebGlProgram, WebGlShader, WebGlTexture,
WebGlUniformLocation,
};
thread_local! {
static FRAME: std::cell::RefCell<u32> = std::cell::RefCell::new(0);
}
const FLOATS_PER_VERT: usize = 5;
enum Batch {
Tri(Vec<f32>),
Line(Vec<f32>),
}
struct State {
ctx: Gl,
canvas: web_sys::OffscreenCanvas,
prog: WebGlProgram,
buf: WebGlBuffer,
u_size: WebGlUniformLocation,
batches: Vec<Batch>,
width: f32,
height: f32,
tex_prog: WebGlProgram,
tex: WebGlTexture,
quad: WebGlBuffer,
u_tex: WebGlUniformLocation,
}
thread_local! {
static STATE: std::cell::RefCell<Option<State>> = std::cell::RefCell::new(None);
}
const VERT: &str = r#"#version 300 es
in vec2 aScreen;
in vec3 aColor;
uniform vec2 uSize;
out vec3 vColor;
void main() {
vec2 ndc = (aScreen / uSize) * 2.0 - 1.0;
ndc.y = -ndc.y;
gl_Position = vec4(ndc, 0.0, 1.0);
vColor = aColor;
}
"#;
const FRAG: &str = r#"#version 300 es
precision mediump float;
in vec3 vColor;
out vec4 fragColor;
void main() {
fragColor = vec4(vColor, 1.0);
}
"#;
const TEX_VERT: &str = r#"#version 300 es
in vec2 aPos;
out vec2 vUv;
void main() {
// aPos spans the NDC quad [-1,1]; map to UV and flip Y so framebuffer row 0
// (top) lands at the top of the canvas.
vUv = vec2((aPos.x + 1.0) * 0.5, 1.0 - (aPos.y + 1.0) * 0.5);
gl_Position = vec4(aPos, 0.0, 1.0);
}
"#;
const TEX_FRAG: &str = r#"#version 300 es
precision mediump float;
in vec2 vUv;
uniform sampler2D uTex;
out vec4 fragColor;
void main() {
fragColor = texture(uTex, vUv);
}
"#;
fn compile_shader(ctx: &Gl, kind: u32, src: &str) -> WebGlShader {
let sh = ctx.create_shader(kind).expect("create_shader");
ctx.shader_source(&sh, src);
ctx.compile_shader(&sh);
assert!(
ctx.get_shader_parameter(&sh, Gl::COMPILE_STATUS)
.as_bool()
.unwrap_or(false),
"shader compile: {}",
ctx.get_shader_info_log(&sh).unwrap_or_default()
);
sh
}
fn link_program(ctx: &Gl, vs: &WebGlShader, fs: &WebGlShader) -> WebGlProgram {
let prog = ctx.create_program().expect("create_program");
ctx.attach_shader(&prog, vs);
ctx.attach_shader(&prog, fs);
ctx.link_program(&prog);
assert!(
ctx.get_program_parameter(&prog, Gl::LINK_STATUS)
.as_bool()
.unwrap_or(false),
"program link: {}",
ctx.get_program_info_log(&prog).unwrap_or_default()
);
prog
}
pub fn init_canvas(canvas: web_sys::OffscreenCanvas) {
let ctx: Gl = canvas
.get_context("webgl2")
.expect("get_context call")
.expect("webgl2 unavailable")
.dyn_into::<Gl>()
.expect("cast to WebGl2RenderingContext");
let vs = compile_shader(&ctx, Gl::VERTEX_SHADER, VERT);
let fs = compile_shader(&ctx, Gl::FRAGMENT_SHADER, FRAG);
let prog = link_program(&ctx, &vs, &fs);
let buf = ctx.create_buffer().expect("create_buffer");
ctx.disable(Gl::DEPTH_TEST);
let u_size = ctx
.get_uniform_location(&prog, "uSize")
.expect("uniform uSize not found");
let tvs = compile_shader(&ctx, Gl::VERTEX_SHADER, TEX_VERT);
let tfs = compile_shader(&ctx, Gl::FRAGMENT_SHADER, TEX_FRAG);
let tex_prog = link_program(&ctx, &tvs, &tfs);
let u_tex = ctx
.get_uniform_location(&tex_prog, "uTex")
.expect("uniform uTex not found");
let tex = ctx.create_texture().expect("create_texture");
let quad = ctx.create_buffer().expect("create_buffer quad");
ctx.bind_buffer(Gl::ARRAY_BUFFER, Some(&quad));
{
let verts: [f32; 12] = [
-1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0,
];
unsafe {
let arr = js_sys::Float32Array::view(&verts);
ctx.buffer_data_with_array_buffer_view(Gl::ARRAY_BUFFER, &arr, Gl::STATIC_DRAW);
}
}
let width = canvas.width() as f32;
let height = canvas.height() as f32;
ctx.viewport(0, 0, canvas.width() as i32, canvas.height() as i32);
STATE.with(|cell| {
*cell.borrow_mut() = Some(State {
ctx,
canvas,
prog,
buf,
u_size,
batches: Vec::new(),
width,
height,
tex_prog,
tex,
quad,
u_tex,
});
});
}
pub fn canvas_size() -> (u32, u32) {
STATE.with(|cell| {
cell.borrow()
.as_ref()
.map(|s| (s.canvas.width(), s.canvas.height()))
.unwrap_or((800, 600))
})
}
pub fn resize(width: u32, height: u32) {
STATE.with(|cell| {
if let Some(s) = cell.borrow_mut().as_mut() {
s.canvas.set_width(width);
s.canvas.set_height(height);
s.width = width as f32;
s.height = height as f32;
s.ctx.viewport(0, 0, width as i32, height as i32);
}
});
}
#[inline]
fn unpack(color: u32) -> (f32, f32, f32) {
(
((color >> 16) & 0xFF) as f32 / 255.0,
((color >> 8) & 0xFF) as f32 / 255.0,
(color & 0xFF) as f32 / 255.0,
)
}
pub fn push_triangle(
color: u32,
x0: f32,
y0: f32,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
_depth: f32,
) {
STATE.with(|cell| {
if let Some(s) = cell.borrow_mut().as_mut() {
let (r, g, b) = unpack(color);
let verts = [x0, y0, r, g, b, x1, y1, r, g, b, x2, y2, r, g, b];
match s.batches.last_mut() {
Some(Batch::Tri(v)) => v.extend_from_slice(&verts),
_ => s.batches.push(Batch::Tri(verts.to_vec())),
}
}
});
}
pub fn push_line(color: u32, x0: f32, y0: f32, x1: f32, y1: f32, _depth: f32) {
STATE.with(|cell| {
if let Some(s) = cell.borrow_mut().as_mut() {
let (r, g, b) = unpack(color);
let verts = [x0, y0, r, g, b, x1, y1, r, g, b];
match s.batches.last_mut() {
Some(Batch::Line(v)) => v.extend_from_slice(&verts),
_ => s.batches.push(Batch::Line(verts.to_vec())),
}
}
});
}
fn upload_and_draw(
ctx: &Gl,
prog: &WebGlProgram,
buf: &WebGlBuffer,
u_size: &WebGlUniformLocation,
verts: &[f32],
w: f32,
h: f32,
mode: u32,
) {
ctx.bind_buffer(Gl::ARRAY_BUFFER, Some(buf));
unsafe {
let arr = js_sys::Float32Array::view(verts);
ctx.buffer_data_with_array_buffer_view(Gl::ARRAY_BUFFER, &arr, Gl::DYNAMIC_DRAW);
}
ctx.use_program(Some(prog));
ctx.uniform2f(Some(u_size), w, h);
let stride = (FLOATS_PER_VERT * 4) as i32;
let loc_screen = ctx.get_attrib_location(prog, "aScreen") as u32;
let loc_color = ctx.get_attrib_location(prog, "aColor") as u32;
ctx.enable_vertex_attrib_array(loc_screen);
ctx.vertex_attrib_pointer_with_i32(loc_screen, 2, Gl::FLOAT, false, stride, 0);
ctx.enable_vertex_attrib_array(loc_color);
ctx.vertex_attrib_pointer_with_i32(loc_color, 3, Gl::FLOAT, false, stride, 8);
ctx.draw_arrays(mode, 0, (verts.len() / FLOATS_PER_VERT) as i32);
}
pub fn flush(fill_r: f32, fill_g: f32, fill_b: f32, width: usize, height: usize) {
STATE.with(|cell| {
let mut opt = cell.borrow_mut();
if let Some(s) = opt.as_mut() {
let ctx = &s.ctx;
ctx.clear_color(fill_r, fill_g, fill_b, 1.0);
ctx.clear(Gl::COLOR_BUFFER_BIT);
let w = width as f32;
let h = height as f32;
let batches = std::mem::take(&mut s.batches);
FRAME.with(|fc| {
let n = *fc.borrow();
if n < 3 {
web_sys::console::log_1(&JsValue::from_str(&format!(
"[ling webgl] frame={n} batches={} canvas={}x{} fill=({fill_r:.2},{fill_g:.2},{fill_b:.2})",
batches.len(), w as u32, h as u32,
)));
}
*fc.borrow_mut() = n + 1;
});
for batch in &batches {
match batch {
Batch::Tri(verts) =>
upload_and_draw(ctx, &s.prog, &s.buf, &s.u_size, verts, w, h, Gl::TRIANGLES),
Batch::Line(verts) =>
upload_and_draw(ctx, &s.prog, &s.buf, &s.u_size, verts, w, h, Gl::LINES),
}
}
ctx.flush();
}
});
}
pub fn blit_rgb(buffer: &[u32], width: usize, height: usize) {
if width == 0 || height == 0 {
return;
}
STATE.with(|cell| {
let mut opt = cell.borrow_mut();
let Some(s) = opt.as_mut() else { return };
let ctx = &s.ctx;
let n = width * height;
let mut px = Vec::with_capacity(n * 4);
for i in 0..n {
let c = buffer.get(i).copied().unwrap_or(0);
px.push(((c >> 16) & 0xFF) as u8);
px.push(((c >> 8) & 0xFF) as u8);
px.push((c & 0xFF) as u8);
px.push(255);
}
ctx.active_texture(Gl::TEXTURE0);
ctx.bind_texture(Gl::TEXTURE_2D, Some(&s.tex));
ctx.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MIN_FILTER, Gl::NEAREST as i32);
ctx.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MAG_FILTER, Gl::NEAREST as i32);
ctx.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_S, Gl::CLAMP_TO_EDGE as i32);
ctx.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_T, Gl::CLAMP_TO_EDGE as i32);
let _ = ctx.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array(
Gl::TEXTURE_2D,
0,
Gl::RGBA as i32,
width as i32,
height as i32,
0,
Gl::RGBA,
Gl::UNSIGNED_BYTE,
Some(&px),
);
ctx.use_program(Some(&s.tex_prog));
ctx.uniform1i(Some(&s.u_tex), 0);
ctx.bind_buffer(Gl::ARRAY_BUFFER, Some(&s.quad));
let loc = ctx.get_attrib_location(&s.tex_prog, "aPos") as u32;
ctx.enable_vertex_attrib_array(loc);
ctx.vertex_attrib_pointer_with_i32(loc, 2, Gl::FLOAT, false, 0, 0);
ctx.draw_arrays(Gl::TRIANGLES, 0, 6);
ctx.flush();
});
}