# ============================================================================ # gl.ludic — Gl.*: OpenGL for Ludic. # # The whole OpenGL 4.1 core API is available as Gl.(...) — every # entry point of the platform gl3.h is bound in gl_api.ludic (generated), with # every GL_* constant. float/double parameters take `fixed`; buffers are the raw # `bytes`/`words` pointers Ludic already has, and pixel/vertex data is uploaded # from them as-is. This file adds the small amount of glue the API needs to be # usable from a game: a context on the window (or an offscreen one headless), # the swap, a screenshot, shader/program helpers, and IEEE float helpers so a # program can fill a vertex buffer with real floats from its Q16.16 math. # # Windowed: the NSOpenGLContext is attached to the existing LudicView (cocoa.ll) # at the display's backing resolution. Headless: a CGL context with no drawable # (gl.ll) and a framebuffer object that stands in for the screen, so the same # program renders and screenshots byte-identically under the test harness. # ============================================================================ import "gl_api.ludic" # ---- native glue (cocoa.ll / gl.ll) ------------------------------------------- extern function win_gl_attach() -> int = "win_gl_attach" extern function win_set_title(title: pointer) -> void = "win_set_title" extern function win_gl_resize(w: int, h: int) = "win_gl_resize" extern function win_gl_swap() = "win_gl_swap" extern function win_gl_scale() -> int = "win_gl_scale" extern function win_gl_swap_interval(n: int) = "win_gl_swap_interval" extern function win_gl_drawable(out: pointer) = "win_gl_drawable" extern function win_gl_update() = "win_gl_update" extern function win_gl_retina(on: int) = "win_gl_retina" extern function win_toggle_fullscreen() = "win_toggle_fullscreen" extern function cgl_offscreen() -> int = "cgl_offscreen" # wall clock in microseconds — the only sub-second clock available to a Ludic program extern function gl_now_us() -> long = "gl_now_us" extern function gl_sleep_us(us: long) -> void = "gl_sleep_us" extern function fx_to_f32(fx: fixed) -> int = "fx_to_f32" extern function f32_to_fx(bits: int) -> fixed = "f32_to_fx" extern function mem_off(p: pointer, off: int) -> pointer = "mem_off" extern function mem_get_i32(p: pointer, off: int) -> int = "mem_get_i32" extern function mem_put_i32(p: pointer, off: int, v: int) = "mem_put_i32" extern function mem_get_u16(p: pointer, off: int) -> int = "mem_get_u16" extern function mem_put_u16(p: pointer, off: int, v: int) = "mem_put_u16" extern function mem_get_u8(p: pointer, off: int) -> int = "mem_get_u8" extern function mem_put_u8(p: pointer, off: int, v: int) = "mem_put_u8" extern function mem_get_f32(p: pointer, i: int) -> fixed = "mem_get_f32" extern function mem_put_f32(p: pointer, i: int, v: fixed) = "mem_put_f32" extern function mem_get_f32_bits(p: pointer, i: int) -> int = "mem_get_f32_bits" extern function mem_put_f32_bits(p: pointer, i: int, bits: int) = "mem_put_f32_bits" extern function mem_copy(dst: pointer, src: pointer, n: int) = "mem_copy" extern function mem_set(dst: pointer, v: int, n: int) = "mem_set" # IEEE-754 single precision, carried as its bit pattern in an int extern function f_add(a: int, b: int) -> int = "f_add" extern function f_sub(a: int, b: int) -> int = "f_sub" extern function f_mul(a: int, b: int) -> int = "f_mul" extern function f_div(a: int, b: int) -> int = "f_div" extern function f_neg(a: int) -> int = "f_neg" extern function f_sqrt(a: int) -> int = "f_sqrt" extern function f_abs(a: int) -> int = "f_abs" extern function f_sin(a: int) -> int = "f_sin" extern function f_cos(a: int) -> int = "f_cos" extern function f_tan(a: int) -> int = "f_tan" extern function f_atan2(a: int, b: int) -> int = "f_atan2" extern function f_pow(a: int, b: int) -> int = "f_pow" extern function f_exp(a: int) -> int = "f_exp" extern function f_log(a: int) -> int = "f_log" extern function f_floor(a: int) -> int = "f_floor" extern function f_mod(a: int, b: int) -> int = "f_mod" extern function f_ldexp(a: int, e: int) -> int = "f_ldexp" extern function f_min(a: int, b: int) -> int = "f_min" extern function f_max(a: int, b: int) -> int = "f_max" extern function f_lt(a: int, b: int) -> int = "f_lt" extern function f_from_int(a: int) -> int = "f_from_int" extern function f_to_int(a: int) -> int = "f_to_int" # ---- state -------------------------------------------------------------------- export state RtGlState { gl_is_open: bool = false gl_w: int = 0 # drawable width, in pixels gl_h: int = 0 gl_scale: int = 1 # backing pixels per window point gl_screen: int = 0 # the framebuffer that is "the screen" (an FBO headless) gl_ids: words = null # one-word scratch for glGen*/glGet* gl_size_buf: words = null gl_log_buf: string = null } function gl_scratch(rt_gl_st: mut RtGlState) -> words { if rt_gl_st.gl_ids == null { rt_gl_st.gl_ids = words(4) } return rt_gl_st.gl_ids } # Open a GL 4.1 core context on a w x h (points) window titled `title`; headless, # an offscreen context with a w x h framebuffer standing in for the screen. function gl_open(rt_gl_st: mut RtGlState, width: int, height: int, title: pointer) -> bool { if rt_gl_st.gl_is_open { return true } if is_windowed() { if win_gl_attach() == 0 { win_open(width, height, 1, title) # a plain program: no window yet if win_gl_attach() == 0 { return false } } else { # a game's window was opened before main, titled with the program's name: give it # the title this call asks for win_set_title(title) } win_gl_resize(width, height) rt_gl_st.gl_scale = win_gl_scale() rt_gl_st.gl_w = width * rt_gl_st.gl_scale rt_gl_st.gl_h = height * rt_gl_st.gl_scale rt_gl_st.gl_screen = 0 } else { if cgl_offscreen() == 0 { return false } rt_gl_st.gl_scale = 1 rt_gl_st.gl_w = width rt_gl_st.gl_h = height rt_gl_st.gl_screen = gl_make_screen_fbo(rt_gl_st, width, height) } gl_bind_framebuffer(GL_FRAMEBUFFER, rt_gl_st.gl_screen) gl_viewport(0, 0, rt_gl_st.gl_w, rt_gl_st.gl_h) rt_gl_st.gl_is_open = true return true } function gl_make_screen_fbo(rt_gl_st: mut RtGlState, w: int, h: int) -> int { let ids = gl_scratch(rt_gl_st) gl_gen_framebuffers(1, ids) let fbo = ids[0] gl_bind_framebuffer(GL_FRAMEBUFFER, fbo) gl_gen_textures(1, ids) let tex = ids[0] gl_bind_texture(GL_TEXTURE_2D, tex) gl_tex_image2d(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, null) gl_tex_parameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) gl_tex_parameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) gl_framebuffer_texture2d(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0) gl_gen_renderbuffers(1, ids) let rb = ids[0] gl_bind_renderbuffer(GL_RENDERBUFFER, rb) gl_renderbuffer_storage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, w, h) gl_framebuffer_renderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rb) return fbo } # Did the drawable change size (a window drag, full screen, a Retina switch)? Then # gl_w / gl_h follow it and the caller rebuilds its screen-sized targets. function gl_resize_check(rt_gl_st: mut RtGlState) -> bool { if not is_windowed() or not rt_gl_st.gl_is_open { return false } if rt_gl_st.gl_size_buf == null { rt_gl_st.gl_size_buf = words(4) } win_gl_drawable(rt_gl_st.gl_size_buf) let w = rt_gl_st.gl_size_buf[0]; let h = rt_gl_st.gl_size_buf[1] if w <= 0 or h <= 0 { return false } if w == rt_gl_st.gl_w and h == rt_gl_st.gl_h { return false } win_gl_update() rt_gl_st.gl_w = w; rt_gl_st.gl_h = h rt_gl_st.gl_scale = win_gl_scale() gl_viewport(0, 0, rt_gl_st.gl_w, rt_gl_st.gl_h) return true } function gl_set_window(w: int, h: int) -> void { if is_windowed() { win_gl_resize(w, h) } } function gl_toggle_fullscreen() -> void { if is_windowed() { win_toggle_fullscreen() } } function gl_set_retina(on: bool) -> void { if is_windowed() { var v = 0; if on { v = 1 }; win_gl_retina(v) } } # vsync on (1, the default) or off (0); headless has nothing to sync to function gl_vsync(n: int) -> void { if is_windowed() { win_gl_swap_interval(n) } } function gl_width(rt_gl_st: RtGlState) -> int { return rt_gl_st.gl_w } function gl_height(rt_gl_st: RtGlState) -> int { return rt_gl_st.gl_h } function gl_screen_fbo(rt_gl_st: RtGlState) -> int { return rt_gl_st.gl_screen } function gl_pixel_scale(rt_gl_st: RtGlState) -> int { return rt_gl_st.gl_scale } # a renderer that owns its swapchain (render3d's Vulkan path) says what the drawable is function gl_set_drawable(rt_gl_st: mut RtGlState, w: int, h: int) -> void { rt_gl_st.gl_w = w rt_gl_st.gl_h = h } function gl_set_pixel_scale(rt_gl_st: mut RtGlState, s: int) -> void { rt_gl_st.gl_scale = s } # Present the frame (vsync'd flushBuffer); headless, just finish the GPU work. function gl_swap() -> void { if is_windowed() { win_gl_swap() } else { gl_finish() } } # Write what is on the screen framebuffer to a binary PPM (call before Gl.swap). function gl_screenshot(rt_gl_st: RtGlState, path: pointer) -> bool { let w = rt_gl_st.gl_w let h = rt_gl_st.gl_h let f = file_open(path, "wb") if f == null { return false } let buf = bytes(w * h * 3) gl_bind_framebuffer(GL_READ_FRAMEBUFFER, rt_gl_st.gl_screen) gl_pixel_storei(GL_PACK_ALIGNMENT, 1) gl_read_pixels(0, 0, w, h, GL_RGB, GL_UNSIGNED_BYTE, buf) let hdr = `P6\n{w} {h}\n255\n` file_write(f, hdr, len(hdr)) var y = h - 1 while y >= 0 { file_write(f, mem_off(buf, y * w * 3), w * 3) y -= 1 } file_close(f) free(buf) return true } # Print any pending GL error under a tag; returns the error code (0 = none). function gl_check(tag: pointer) -> int { let e = gl_get_error() if e != 0 { print(`gl error {e} at {tag}`) } return e } # ---- shaders -------------------------------------------------------------------- # Compile one shader stage from source; 0 (and the info log on stdout) on failure. function gl_shader(rt_gl_st: mut RtGlState, kind: int, src: pointer) -> int { let id = gl_create_shader(kind) var srcs: pointers = pointers(1) srcs[0] = src gl_shader_source(id, 1, srcs, null) gl_compile_shader(id) let ids = gl_scratch(rt_gl_st) gl_get_shaderiv(id, GL_COMPILE_STATUS, ids) if ids[0] == 0 { if rt_gl_st.gl_log_buf == null { rt_gl_st.gl_log_buf = bytes(8192) } gl_get_shader_info_log(id, 8191, null, rt_gl_st.gl_log_buf) print("shader compile failed:") print(rt_gl_st.gl_log_buf) gl_delete_shader(id) return 0 } return id } # Link a program from a vertex + fragment source pair; 0 on failure. function gl_program(rt_gl_st: mut RtGlState, vs: pointer, fs: pointer) -> int { return gl_program5(rt_gl_st, vs, null, null, null, fs) } # Link a program from up to five stages (null = stage absent). function gl_program5(rt_gl_st: mut RtGlState, vs: pointer, tcs: pointer, tes: pointer, gs: pointer, fs: pointer) -> int { let prog = gl_create_program() var ok = true if vs != null { let s = gl_shader(rt_gl_st, GL_VERTEX_SHADER, vs); if s == 0 { ok = false } else { gl_attach_shader(prog, s) } } if tcs != null { let s = gl_shader(rt_gl_st, GL_TESS_CONTROL_SHADER, tcs); if s == 0 { ok = false } else { gl_attach_shader(prog, s) } } if tes != null { let s = gl_shader(rt_gl_st, GL_TESS_EVALUATION_SHADER, tes); if s == 0 { ok = false } else { gl_attach_shader(prog, s) } } if gs != null { let s = gl_shader(rt_gl_st, GL_GEOMETRY_SHADER, gs); if s == 0 { ok = false } else { gl_attach_shader(prog, s) } } if fs != null { let s = gl_shader(rt_gl_st, GL_FRAGMENT_SHADER, fs); if s == 0 { ok = false } else { gl_attach_shader(prog, s) } } if not ok { gl_delete_program(prog); return 0 } gl_link_program(prog) let ids = gl_scratch(rt_gl_st) gl_get_programiv(prog, GL_LINK_STATUS, ids) if ids[0] == 0 { if rt_gl_st.gl_log_buf == null { rt_gl_st.gl_log_buf = bytes(8192) } gl_get_program_info_log(prog, 8191, null, rt_gl_st.gl_log_buf) print("program link failed:") print(rt_gl_st.gl_log_buf) gl_delete_program(prog) return 0 } return prog } function gl_uniform(prog: int, name: pointer) -> int { return gl_get_uniform_location(prog, name) } # ---- buffers of floats ------------------------------------------------------------ # A float buffer is plain memory: n IEEE floats, filled from fixed (Gl.put) or # from float bits (Gl.put_bits), uploaded with Gl.buffer_data(…, Gl.bytes_of(n), buf, …). function gl_floats(n: int) -> pointer { return bytes(n * 4) } function gl_bytes_of(n: int) -> int { return n * 4 } function gl_put(buf: pointer, i: int, v: fixed) -> void { mem_put_f32(buf, i, v) } function gl_get(buf: pointer, i: int) -> fixed { return mem_get_f32(buf, i) } function gl_put_bits(buf: pointer, i: int, bits: int) -> void { mem_put_f32_bits(buf, i, bits) } function gl_get_bits(buf: pointer, i: int) -> int { return mem_get_f32_bits(buf, i) } function gl_ptr(buf: pointer, byte_offset: int) -> pointer { return mem_off(buf, byte_offset) } function gl_f32(v: fixed) -> int { return fx_to_f32(v) } function gl_fixed(bits: int) -> fixed { return f32_to_fx(bits) } # One VAO, one VBO helper: create a vertex array object and return it, bound. function gl_vao(rt_gl_st: mut RtGlState) -> int { let ids = gl_scratch(rt_gl_st) gl_gen_vertex_arrays(1, ids) gl_bind_vertex_array(ids[0]) return ids[0] } function gl_buffer(rt_gl_st: mut RtGlState) -> int { let ids = gl_scratch(rt_gl_st) gl_gen_buffers(1, ids) return ids[0] } function gl_texture(rt_gl_st: mut RtGlState) -> int { let ids = gl_scratch(rt_gl_st) gl_gen_textures(1, ids) return ids[0] } function gl_framebuffer(rt_gl_st: mut RtGlState) -> int { let ids = gl_scratch(rt_gl_st) gl_gen_framebuffers(1, ids) return ids[0] } # Time.now_us / Time.sleep_us (runtime/native/namespaces.ludic): the platform's clock, as an API a # program reaches without calling C (L7) function time_now_us() -> long { return gl_now_us() } function time_sleep_us(us: long) -> void { gl_sleep_us(us) }