feat(windows): Gl.* on Windows, through WGL and a driver-filled thunk table

gl_win.ll creates a hidden-window WGL 4.1 core context and carries gl.ll's
float/memory helpers, with ldexp and QueryPerformanceCounter in place of the
libSystem calls. glgen now also writes gl_thunks_win.ll: the same 478 thunks,
calling through pointers that @lgl_win_load fills from wglGetProcAddress (and
opengl32.dll for GL 1.1). ludicc links the pair against opengl32/gdi32/user32
on a Windows target.

Verified: gl_api.ludic and gl_thunks.ll regenerate byte-identically, ludic-dev
test 135/135, selfhost-test 32/32, and headless gl_triangle on an RTX 3070 Ti
matches the macOS frame to within one level per channel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-13 02:10:36 +03:00
parent 5801005fca
commit cb05721a89
6 changed files with 13177 additions and 301 deletions

View file

@ -530,13 +530,138 @@ function cmd_glgen() -> int {
var api_out = "runtime/native/gl_api.ludic"
var thunk_out = "runtime/native/gl_thunks.ll"
if check { api_out = tmp_path("gl_api.ludic"); thunk_out = tmp_path("gl_thunks.ll") }
var win_out = "runtime/native/gl_thunks_win.ll"
if check { api_out = tmp_path("gl_api.ludic"); thunk_out = tmp_path("gl_thunks.ll"); win_out = tmp_path("gl_thunks_win.ll") }
if not glg_emit_ludic(api_out) { err("glgen: cannot write gl_api.ludic\n"); return 1 }
if not glg_emit_thunks(thunk_out) { err("glgen: cannot write gl_thunks.ll\n"); return 1 }
if not glg_emit_thunks_win(win_out) { err("glgen: cannot write gl_thunks_win.ll\n"); return 1 }
if check {
if not shq(`cmp -s {api_out} runtime/native/gl_api.ludic`) { err("gl_api.ludic drifted from gl3.h (run: ludic-dev glgen)\n"); return 1 }
if not shq(`cmp -s {thunk_out} runtime/native/gl_thunks.ll`) { err("gl_thunks.ll drifted from gl3.h (run: ludic-dev glgen)\n"); return 1 }
if not shq(`cmp -s {win_out} runtime/native/gl_thunks_win.ll`) { err("gl_thunks_win.ll drifted from gl3.h (run: ludic-dev glgen)\n"); return 1 }
}
print(`OK {string(len(glg_cname))} constants, {string(len(glg_fname))} entry points`)
return 0
}
# ---- emit gl_thunks_win.ll --------------------------------------------------
# The Windows thunks. Same Ludic ABI and the same conversions as gl_thunks.ll, but
# no thunk names a gl* symbol: opengl32.dll exports only OpenGL 1.1, and every later
# entry point belongs to the driver, reachable only through wglGetProcAddress while
# a context is current. So each thunk calls through @lgl_p_<name>, and
# @lgl_win_load (called by gl_win.ll once its 4.1 core context is current) fills
# the table - from the driver first, then from opengl32.dll for the 1.1 functions,
# which wglGetProcAddress does not return.
function glg_emit_thunks_win(path: pointer) -> bool {
let b = sb_new()
sb_puts(b, "; ============================================================================\n")
sb_puts(b, "; gl_thunks_win.ll — one thunk per OpenGL 4.1 core entry point, for Windows.\n")
sb_puts(b, "; GENERATED by `ludic-dev glgen` from gl3.h. The thunks of gl_thunks.ll, calling\n")
sb_puts(b, "; through @lgl_p_* pointers that @lgl_win_load fills from the current context.\n")
sb_puts(b, "; ============================================================================\n")
sb_puts(b, "\n")
sb_puts(b, "declare ptr @wglGetProcAddress(ptr)\n")
sb_puts(b, "declare ptr @LoadLibraryA(ptr)\n")
sb_puts(b, "declare ptr @GetProcAddress(ptr, ptr)\n")
sb_puts(b, "@.lgl_dll = private unnamed_addr constant [13 x i8] c\"opengl32.dll\\00\"\n")
var i = 0
while i < len(glg_fname) {
let nm = glg_fname[i]
sb_puts(b, `@lgl_p_{nm} = internal global ptr null\n`)
sb_puts(b, `@.lgl_n_{nm} = private unnamed_addr constant [{string(len(nm) + 3)} x i8] c"gl{nm}\\00"\n`)
i += 1
}
sb_puts(b, "\n")
i = 0
while i < len(glg_fname) {
let cret = glg_ir_ty(glg_frbase[i], glg_frstar[i])
let lret = glg_abi_ty(cret)
let sig = sb_new()
let body = sb_new()
let call = sb_new()
var p = 0
while p < glg_fpcnt[i] {
let ix = glg_fpoff[i] + p
let ct = glg_ir_ty(glg_ppbase[ix], glg_ppstar[ix])
let lt = glg_abi_ty(ct)
if p > 0 { sb_puts(sig, ", "); sb_puts(call, ", ") }
sb_puts(sig, `{lt} %a{string(p)}`)
if ct == lt {
sb_puts(call, `{ct} %a{string(p)}`)
} else {
if ct == "i8" or ct == "i16" {
sb_puts(body, ` %c{string(p)} = trunc i32 %a{string(p)} to {ct}\n`)
sb_puts(call, `{ct} %c{string(p)}`)
} else {
sb_puts(body, ` %f{string(p)} = sitofp i32 %a{string(p)} to {ct}\n`)
sb_puts(body, ` %c{string(p)} = fmul {ct} %f{string(p)}, 0x3EF0000000000000\n`)
sb_puts(call, `{ct} %c{string(p)}`)
}
}
p += 1
}
sb_puts(b, `define {lret} @lgl_{glg_fname[i]}({sb_str(sig)}) `)
sb_puts(b, "{\n")
sb_puts(b, "entry:\n")
sb_puts(b, ` %fp = load ptr, ptr @lgl_p_{glg_fname[i]}\n`)
sb_puts(b, sb_str(body))
let invoke = `call {cret} %fp({sb_str(call)})`
if cret == "void" {
sb_puts(b, ` {invoke}\n`)
sb_puts(b, " ret void\n")
} else {
if cret == lret {
sb_puts(b, ` %r = {invoke}\n`)
sb_puts(b, ` ret {lret} %r\n`)
} else {
if cret == "i8" or cret == "i16" {
sb_puts(b, ` %r = {invoke}\n`)
sb_puts(b, ` %z = zext {cret} %r to i32\n`)
sb_puts(b, " ret i32 %z\n")
} else {
sb_puts(b, ` %r = {invoke}\n`)
sb_puts(b, ` %m = fmul {cret} %r, 65536.0\n`)
sb_puts(b, ` %z = fptosi {cret} %m to i32\n`)
sb_puts(b, " ret i32 %z\n")
}
}
}
sb_puts(b, "}\n")
i += 1
}
# the loader: wglGetProcAddress returns 0, 1, 2, 3 or -1 for a name it does not
# know (drivers differ), and never answers for a 1.1 function; both fall back to
# opengl32.dll's own export. Returns how many names are still unresolved.
sb_puts(b, "\ndefine i32 @lgl_win_load() {\n")
sb_puts(b, "entry:\n %dll = call ptr @LoadLibraryA(ptr @.lgl_dll)\n br label %f0\n")
i = 0
while i < len(glg_fname) {
let nm = glg_fname[i]
let k = string(i)
sb_puts(b, `f{k}:\n %m{k} = phi i32 [ {glg_load_in(i)} ]\n`)
sb_puts(b, ` %w{k} = call ptr @wglGetProcAddress(ptr @.lgl_n_{nm})\n`)
sb_puts(b, ` %wi{k} = ptrtoint ptr %w{k} to i64\n %wa{k} = add i64 %wi{k}, 1\n %bad{k} = icmp ult i64 %wa{k}, 5\n`)
sb_puts(b, ` br i1 %bad{k}, label %g{k}, label %s{k}\n`)
sb_puts(b, `g{k}:\n %e{k} = call ptr @GetProcAddress(ptr %dll, ptr @.lgl_n_{nm})\n br label %s{k}\n`)
sb_puts(b, `s{k}:\n %p{k} = phi ptr [ %w{k}, %f{k} ], [ %e{k}, %g{k} ]\n store ptr %p{k}, ptr @lgl_p_{nm}\n`)
sb_puts(b, ` %u{k} = icmp eq ptr %p{k}, null\n %ui{k} = zext i1 %u{k} to i32\n %m{string(i + 1)}x = add i32 %m{k}, %ui{k}\n`)
sb_puts(b, ` br label %f{string(i + 1)}\n`)
i += 1
}
let n = string(len(glg_fname))
sb_puts(b, `f{n}:\n %m{n} = phi i32 [ %m{n}x, %s{glg_prev_idx(len(glg_fname))} ]\n ret i32 %m{n}\n`)
sb_puts(b, "}\n")
return write_file(path, sb_str(b))
}
# the loader's blocks: f0 is entered only from `entry` with a count of 0, every
# later fN only from s(N-1), carrying the count that block computed
function glg_load_in(i: int) -> pointer {
if i == 0 { return "0, %entry" }
return `%m{string(i)}x, %s{string(i - 1)}`
}
function glg_prev_idx(i: int) -> pointer {
if i == 0 { return "0" }
return string(i - 1)
}