feat(windows): Http.* over WinHTTP, and the splash and window icon through WIC

http_win.ll implements http.ll's hs_* contract over WinHTTP: the request is a
record built on the game thread, the whole exchange runs on a worker thread,
TLS uses the system's certificate checks, and headers are answered from the
kept request handle. ludicc links it with winhttp instead of refusing Http.* on
Windows.

win32.ll decodes the splash and App.set_icon images with WIC (ole32): the
splash is a topmost borderless window at the artwork's size in points times the
display scale, composited over its background colour; the icon becomes an
HICON set on the window now or when win_open makes one.

Verified on the PC: examples/library/http.ludic prints its expected output, a
real GET to https://git.workshopsoft.io/ returns 200 with its body and
Content-Type, and the bundled Maroon Lake shows its splash centred at launch and
its icon in the title bar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-13 03:24:50 +03:00
parent 55b0e2ebf6
commit 40d78cbd92
4 changed files with 988 additions and 12 deletions

View file

@ -237,6 +237,7 @@ show:
store ptr %hwnd, ptr @W_hwnd
%hdc = call ptr @GetDC(ptr %hwnd)
store ptr %hdc, ptr @W_hdc
call void @w_apply_icon() ; an App.set_icon that ran before there was a window
%s1 = call i32 @ShowWindow(ptr %hwnd, i32 5) ; SW_SHOW
%s2 = call i32 @SetForegroundWindow(ptr %hwnd)
; RAWINPUTDEVICE { usUsagePage 1, usUsage 2 (mouse), dwFlags 0, hwndTarget }: the raw
@ -893,19 +894,452 @@ entry:
ret void
}
; ---- the splash and the icon ---------------------------------------------------
; Not yet: both need an image decoder (WIC), and on Windows the icon a player sees on
; the taskbar and in Explorer is the one linked into the .exe, which `ludic bundle`
; will do. Until then these keep the contract - safe to call, change nothing.
; ---- images: a PNG or JPEG decoded by WIC --------------------------------------
;
; The splash and the window icon arrive as file bytes (out of the pack), and WIC -
; the codecs every Windows install carries - turns them into 32-bit BGRA, as NSImage
; does on macOS. COM slots, from wincodec.h:
; IWICImagingFactory 4 CreateDecoderFromStream 10 CreateFormatConverter 14 CreateStream
; IWICStream 16 InitializeFromMemory
; IWICBitmapDecoder 13 GetFrame
; IWICBitmapFrameDecode 5 GetResolution
; IWICFormatConverter 3 GetSize 7 CopyPixels 8 Initialize
; and 2 Release on all of them.
declare i32 @CoInitializeEx(ptr, i32)
declare i32 @CoCreateInstance(ptr, ptr, i32, ptr, ptr)
declare ptr @CreateDIBSection(ptr, ptr, i32, ptr, ptr, i32)
declare ptr @CreateBitmap(i32, i32, i32, i32, ptr)
declare ptr @CreateIconIndirect(ptr)
declare i32 @DeleteObject(ptr)
declare i64 @SendMessageA(ptr, i32, i64, i64)
declare i32 @DestroyWindow(ptr)
declare i32 @UpdateWindow(ptr)
declare ptr @BeginPaint(ptr, ptr)
declare i32 @EndPaint(ptr, ptr)
declare void @llvm.memcpy.p0.p0.i64(ptr, ptr, i64, i1)
; CLSID_WICImagingFactory {cacaf262-9370-4615-a13b-9f5539da4c0a},
; IID_IWICImagingFactory {ec5ec8a9-c395-4314-9c77-54d7a935ff70},
; GUID_WICPixelFormat32bppBGRA {6fddc324-4e03-4bfe-b185-3d77768dc90f} - in memory order
@.wic_clsid = private unnamed_addr constant [16 x i8] c"\62\F2\CA\CA\70\93\15\46\A1\3B\9F\55\39\DA\4C\0A"
@.wic_iid = private unnamed_addr constant [16 x i8] c"\A9\C8\5E\EC\95\C3\14\43\9C\77\54\D7\A9\35\FF\70"
@.wic_bgra = private unnamed_addr constant [16 x i8] c"\24\C3\DD\6F\03\4E\FE\4B\B1\85\3D\77\76\8D\C9\0F"
@.w_splash_class = private unnamed_addr constant [12 x i8] c"LudicSplash\00"
@.w_empty = private unnamed_addr constant [1 x i8] c"\00"
define internal ptr @w_vt(ptr %obj, i64 %slot) {
entry:
%vt = load ptr, ptr %obj
%p = getelementptr ptr, ptr %vt, i64 %slot
%f = load ptr, ptr %p
ret ptr %f
}
define internal void @w_release(ptr %pp) {
entry:
%o = load ptr, ptr %pp
%none = icmp eq ptr %o, null
br i1 %none, label %out, label %go
go:
%rel = call ptr @w_vt(ptr %o, i64 2)
%r = call i32 %rel(ptr %o)
store ptr null, ptr %pp
br label %out
out:
ret void
}
; bytes -> a malloc'd w x h BGRA buffer (straight alpha), its size, and the image's own
; horizontal dpi (72 when it names none); null when WIC cannot read it
define internal ptr @w_decode(ptr %bytes, i32 %len, ptr %wout, ptr %hout, ptr %dpiout) {
entry:
%fp = alloca ptr, align 8
%sp = alloca ptr, align 8
%dp = alloca ptr, align 8
%frp = alloca ptr, align 8
%cp = alloca ptr, align 8
%wp = alloca i32, align 4
%hp = alloca i32, align 4
%dx = alloca double, align 8
%dy = alloca double, align 8
store ptr null, ptr %fp
store ptr null, ptr %sp
store ptr null, ptr %dp
store ptr null, ptr %frp
store ptr null, ptr %cp
store double 0.000000e+00, ptr %dx
%nob = icmp eq ptr %bytes, null
%empty = icmp sle i32 %len, 0
%skip = or i1 %nob, %empty
br i1 %skip, label %fail, label %com
com:
; COINIT_APARTMENTTHREADED; a thread already initialised answers S_FALSE or RPC_E_CHANGED_MODE, both fine
%ci = call i32 @CoInitializeEx(ptr null, i32 2)
%hr0 = call i32 @CoCreateInstance(ptr @.wic_clsid, ptr null, i32 1, ptr @.wic_iid, ptr %fp) ; CLSCTX_INPROC_SERVER
%b0 = icmp slt i32 %hr0, 0
br i1 %b0, label %fail, label %stream
stream:
%f = load ptr, ptr %fp
%cs = call ptr @w_vt(ptr %f, i64 14)
%hr1 = call i32 %cs(ptr %f, ptr %sp)
%b1 = icmp slt i32 %hr1, 0
br i1 %b1, label %fail, label %meminit
meminit:
%s = load ptr, ptr %sp
%im = call ptr @w_vt(ptr %s, i64 16)
%hr2 = call i32 %im(ptr %s, ptr %bytes, i32 %len)
%b2 = icmp slt i32 %hr2, 0
br i1 %b2, label %fail, label %decoder
decoder:
%cdfs = call ptr @w_vt(ptr %f, i64 4)
%hr3 = call i32 %cdfs(ptr %f, ptr %s, ptr null, i32 0, ptr %dp) ; WICDecodeMetadataCacheOnDemand
%b3 = icmp slt i32 %hr3, 0
br i1 %b3, label %fail, label %frame
frame:
%d = load ptr, ptr %dp
%gf = call ptr @w_vt(ptr %d, i64 13)
%hr4 = call i32 %gf(ptr %d, i32 0, ptr %frp)
%b4 = icmp slt i32 %hr4, 0
br i1 %b4, label %fail, label %resolution
resolution:
%fr = load ptr, ptr %frp
%gr = call ptr @w_vt(ptr %fr, i64 5)
%hr5 = call i32 %gr(ptr %fr, ptr %dx, ptr %dy)
%conv = call ptr @w_vt(ptr %f, i64 10)
%hr6 = call i32 %conv(ptr %f, ptr %cp)
%b6 = icmp slt i32 %hr6, 0
br i1 %b6, label %fail, label %convert
convert:
%c = load ptr, ptr %cp
%ini = call ptr @w_vt(ptr %c, i64 8)
; to 32bpp BGRA, no dither, no palette, alpha threshold 0, WICBitmapPaletteTypeCustom
%hr7 = call i32 %ini(ptr %c, ptr %fr, ptr @.wic_bgra, i32 0, ptr null, double 0.000000e+00, i32 0)
%b7 = icmp slt i32 %hr7, 0
br i1 %b7, label %fail, label %size
size:
%gs = call ptr @w_vt(ptr %c, i64 3)
%hr8 = call i32 %gs(ptr %c, ptr %wp, ptr %hp)
%w = load i32, ptr %wp
%h = load i32, ptr %hp
%zw = icmp sle i32 %w, 0
%zh = icmp sle i32 %h, 0
%zero = or i1 %zw, %zh
br i1 %zero, label %fail, label %pixels
pixels:
%w64 = sext i32 %w to i64
%h64 = sext i32 %h to i64
%px = mul i64 %w64, %h64
%bytesz = mul i64 %px, 4
%buf = call ptr @malloc(i64 %bytesz)
%stride = mul i32 %w, 4
%bsz32 = trunc i64 %bytesz to i32
%cpx = call ptr @w_vt(ptr %c, i64 7)
%hr9 = call i32 %cpx(ptr %c, ptr null, i32 %stride, i32 %bsz32, ptr %buf)
%b9 = icmp slt i32 %hr9, 0
br i1 %b9, label %freebuf, label %ok
freebuf:
call void @free(ptr %buf)
br label %fail
ok:
store i32 %w, ptr %wout
store i32 %h, ptr %hout
%dpd = load double, ptr %dx
%dpi0 = fptosi double %dpd to i32
%nodpi = icmp slt i32 %dpi0, 1
%dpi = select i1 %nodpi, i32 72, i32 %dpi0
store i32 %dpi, ptr %dpiout
call void @w_release(ptr %cp)
call void @w_release(ptr %frp)
call void @w_release(ptr %dp)
call void @w_release(ptr %sp)
call void @w_release(ptr %fp)
ret ptr %buf
fail:
call void @w_release(ptr %cp)
call void @w_release(ptr %frp)
call void @w_release(ptr %dp)
call void @w_release(ptr %sp)
call void @w_release(ptr %fp)
ret ptr null
}
; ---- the boot splash ---------------------------------------------------------------
;
; cocoa.ll's splash, on Windows: a borderless, topmost window with the game's artwork,
; shown from lp_pak_boot before main and taken down by App.splash_hide(). It takes the
; artwork's own size in points (pixels x 72 / its dpi, as NSImage measures it) at the
; display's whole-number scale, so a 1440 x 810 image saved at 144 dpi is 720 x 405
; points on either platform. The image is composited over the background colour once,
; and painted from that on every WM_PAINT.
@W_splash = internal global ptr null
@W_splash_px = internal global ptr null
@W_splash_w = internal global i32 0
@W_splash_h = internal global i32 0
@W_splash_dw = internal global i32 0
@W_splash_dh = internal global i32 0
define internal i64 @w_splash_proc(ptr %hwnd, i32 %msg, i64 %wp, i64 %lp) {
entry:
%ps = alloca [72 x i8], align 8 ; PAINTSTRUCT
%bmi = alloca [44 x i8], align 4
switch i32 %msg, label %def [
i32 15, label %paint ; WM_PAINT
i32 20, label %erase ; WM_ERASEBKGND
]
erase:
ret i64 1
paint:
%hdc = call ptr @BeginPaint(ptr %hwnd, ptr %ps)
%px = load ptr, ptr @W_splash_px
%nopx = icmp eq ptr %px, null
br i1 %nopx, label %endp, label %blit
blit:
%w = load i32, ptr @W_splash_w
%h = load i32, ptr @W_splash_h
%dw = load i32, ptr @W_splash_dw
%dh = load i32, ptr @W_splash_dh
call void @llvm.memset.p0.i64(ptr %bmi, i8 0, i64 44, i1 false)
store i32 40, ptr %bmi
%bw = getelementptr i8, ptr %bmi, i64 4
store i32 %w, ptr %bw
%nh = sub i32 0, %h
%bh = getelementptr i8, ptr %bmi, i64 8
store i32 %nh, ptr %bh
%bp = getelementptr i8, ptr %bmi, i64 12
store i16 1, ptr %bp
%bb = getelementptr i8, ptr %bmi, i64 14
store i16 32, ptr %bb
%r = call i32 @StretchDIBits(ptr %hdc, i32 0, i32 0, i32 %dw, i32 %dh, i32 0, i32 0, i32 %w, i32 %h, ptr %px, ptr %bmi, i32 0, i32 13369376)
br label %endp
endp:
%e = call i32 @EndPaint(ptr %hwnd, ptr %ps)
ret i64 0
def:
%d = call i64 @DefWindowProcA(ptr %hwnd, i32 %msg, i64 %wp, i64 %lp)
ret i64 %d
}
define void @splash_show(ptr %bytes, i32 %len, i32 %bg) {
entry:
%wp = alloca i32, align 4
%hp = alloca i32, align 4
%dpip = alloca i32, align 4
%wc = alloca [80 x i8], align 8
%had = load ptr, ptr @W_splash
%again = icmp ne ptr %had, null
br i1 %again, label %out, label %decode
decode:
%px = call ptr @w_decode(ptr %bytes, i32 %len, ptr %wp, ptr %hp, ptr %dpip)
%nopx = icmp eq ptr %px, null
br i1 %nopx, label %out, label %over
over:
%w = load i32, ptr %wp
%h = load i32, ptr %hp
; composite over the background colour, so transparent edges sit on the game's backdrop
%bgb = and i32 %bg, 255
%bgg0 = lshr i32 %bg, 8
%bgg = and i32 %bgg0, 255
%bgr0 = lshr i32 %bg, 16
%bgr = and i32 %bgr0, 255
%w64 = sext i32 %w to i64
%h64 = sext i32 %h to i64
%n = mul i64 %w64, %h64
br label %mix
mix:
%i = phi i64 [ 0, %over ], [ %i1, %mixb ]
%more = icmp slt i64 %i, %n
br i1 %more, label %mixb, label %placed
mixb:
%o = mul i64 %i, 4
%pb = getelementptr i8, ptr %px, i64 %o
%o1 = add i64 %o, 1
%pg = getelementptr i8, ptr %px, i64 %o1
%o2 = add i64 %o, 2
%pr = getelementptr i8, ptr %px, i64 %o2
%o3 = add i64 %o, 3
%pa = getelementptr i8, ptr %px, i64 %o3
%a8 = load i8, ptr %pa
%a = zext i8 %a8 to i32
%ia = sub i32 255, %a
%sb8 = load i8, ptr %pb
%sb = zext i8 %sb8 to i32
%mb0 = mul i32 %sb, %a
%mb1 = mul i32 %bgb, %ia
%mb2 = add i32 %mb0, %mb1
%mb = udiv i32 %mb2, 255
%tb = trunc i32 %mb to i8
store i8 %tb, ptr %pb
%sg8 = load i8, ptr %pg
%sg = zext i8 %sg8 to i32
%mg0 = mul i32 %sg, %a
%mg1 = mul i32 %bgg, %ia
%mg2 = add i32 %mg0, %mg1
%mg = udiv i32 %mg2, 255
%tg = trunc i32 %mg to i8
store i8 %tg, ptr %pg
%sr8 = load i8, ptr %pr
%sr = zext i8 %sr8 to i32
%mr0 = mul i32 %sr, %a
%mr1 = mul i32 %bgr, %ia
%mr2 = add i32 %mr0, %mr1
%mr = udiv i32 %mr2, 255
%tr = trunc i32 %mr to i8
store i8 %tr, ptr %pr
store i8 -1, ptr %pa
%i1 = add i64 %i, 1
br label %mix
placed:
store ptr %px, ptr @W_splash_px
store i32 %w, ptr @W_splash_w
store i32 %h, ptr @W_splash_h
; points = pixels x 72 / dpi; pixels on screen = points x the display's scale
%dpi = load i32, ptr %dpip
%pw0 = mul i32 %w, 72
%ptw = sdiv i32 %pw0, %dpi
%ph0 = mul i32 %h, 72
%pth = sdiv i32 %ph0, %dpi
%d0 = call i32 @SetProcessDpiAwarenessContext(ptr inttoptr (i64 -4 to ptr))
%sysdpi = call i32 @GetDpiForSystem()
%ds = call i32 @w_fit_scale(i32 %sysdpi, i32 %ptw, i32 %pth)
%dw = mul i32 %ptw, %ds
%dh = mul i32 %pth, %ds
store i32 %dw, ptr @W_splash_dw
store i32 %dh, ptr @W_splash_dh
%inst = call ptr @GetModuleHandleA(ptr null)
call void @llvm.memset.p0.i64(ptr %wc, i8 0, i64 80, i1 false)
store i32 80, ptr %wc
%wwp = getelementptr i8, ptr %wc, i64 8
store ptr @w_splash_proc, ptr %wwp
%whi = getelementptr i8, ptr %wc, i64 24
store ptr %inst, ptr %whi
%arrow = call ptr @LoadCursorA(ptr null, ptr inttoptr (i64 32512 to ptr))
%whc = getelementptr i8, ptr %wc, i64 40
store ptr %arrow, ptr %whc
%wcn = getelementptr i8, ptr %wc, i64 64
store ptr @.w_splash_class, ptr %wcn
%atom = call i16 @RegisterClassExA(ptr %wc)
%sw = call i32 @GetSystemMetrics(i32 0)
%sh = call i32 @GetSystemMetrics(i32 1)
%xw = sub i32 %sw, %dw
%x = sdiv i32 %xw, 2
%yh = sub i32 %sh, %dh
%y = sdiv i32 %yh, 2
; WS_EX_TOPMOST | WS_EX_TOOLWINDOW (no taskbar button), WS_POPUP | WS_VISIBLE
%win = call ptr @CreateWindowExA(i32 136, ptr @.w_splash_class, ptr @.w_empty, i32 -1879048192, i32 %x, i32 %y, i32 %dw, i32 %dh, ptr null, ptr null, ptr %inst, ptr null)
%nowin = icmp eq ptr %win, null
br i1 %nowin, label %out, label %show
show:
store ptr %win, ptr @W_splash
; paint it now: the game is about to spend a while loading and pumps nothing meanwhile
%u = call i32 @UpdateWindow(ptr %win)
br label %out
out:
ret void
}
define void @splash_hide() {
entry:
%win = load ptr, ptr @W_splash
%none = icmp eq ptr %win, null
br i1 %none, label %out, label %go
go:
%d = call i32 @DestroyWindow(ptr %win)
store ptr null, ptr @W_splash
%px = load ptr, ptr @W_splash_px
call void @free(ptr %px)
store ptr null, ptr @W_splash_px
br label %out
out:
ret void
}
; ---- the window icon ---------------------------------------------------------------
;
; App.set_icon(path): the title bar and the taskbar button of a build with no icon
; resource - which is every `ludic build`; a bundled .exe carries its own. The image
; becomes an HICON from a 32-bit DIB with its alpha, set on the window now, or as soon
; as win_open makes one.
@W_icon = internal global ptr null
declare void @free(ptr)
declare ptr @malloc(i64)
define internal void @w_apply_icon() {
entry:
%hwnd = load ptr, ptr @W_hwnd
%icon = load ptr, ptr @W_icon
%nw = icmp eq ptr %hwnd, null
%ni = icmp eq ptr %icon, null
%skip = or i1 %nw, %ni
br i1 %skip, label %out, label %set
set:
%iv = ptrtoint ptr %icon to i64
%r1 = call i64 @SendMessageA(ptr %hwnd, i32 128, i64 1, i64 %iv) ; WM_SETICON, ICON_BIG
%r0 = call i64 @SendMessageA(ptr %hwnd, i32 128, i64 0, i64 %iv) ; ICON_SMALL
br label %out
out:
ret void
}
define void @app_set_icon(ptr %bytes, i32 %len) {
entry:
%wp = alloca i32, align 4
%hp = alloca i32, align 4
%dpip = alloca i32, align 4
%bmi = alloca [44 x i8], align 4
%bitsp = alloca ptr, align 8
%ii = alloca [32 x i8], align 8 ; ICONINFO: fIcon 0, hbmMask 16, hbmColor 24
%px = call ptr @w_decode(ptr %bytes, i32 %len, ptr %wp, ptr %hp, ptr %dpip)
%nopx = icmp eq ptr %px, null
br i1 %nopx, label %out, label %dib
dib:
%w = load i32, ptr %wp
%h = load i32, ptr %hp
call void @llvm.memset.p0.i64(ptr %bmi, i8 0, i64 44, i1 false)
store i32 40, ptr %bmi
%bw = getelementptr i8, ptr %bmi, i64 4
store i32 %w, ptr %bw
%nh = sub i32 0, %h
%bh = getelementptr i8, ptr %bmi, i64 8
store i32 %nh, ptr %bh
%bpl = getelementptr i8, ptr %bmi, i64 12
store i16 1, ptr %bpl
%bbc = getelementptr i8, ptr %bmi, i64 14
store i16 32, ptr %bbc
store ptr null, ptr %bitsp
%color = call ptr @CreateDIBSection(ptr null, ptr %bmi, i32 0, ptr %bitsp, ptr null, i32 0)
%nocolor = icmp eq ptr %color, null
br i1 %nocolor, label %freepx, label %fill
fill:
%bits = load ptr, ptr %bitsp
%w64 = sext i32 %w to i64
%h64 = sext i32 %h to i64
%n = mul i64 %w64, %h64
%nb = mul i64 %n, 4
call void @llvm.memcpy.p0.p0.i64(ptr %bits, ptr %px, i64 %nb, i1 false)
; with an alpha channel in the colour bitmap, an all-zero mask leaves every pixel to it
%mask = call ptr @CreateBitmap(i32 %w, i32 %h, i32 1, i32 1, ptr null)
call void @llvm.memset.p0.i64(ptr %ii, i8 0, i64 32, i1 false)
store i32 1, ptr %ii
%mp = getelementptr i8, ptr %ii, i64 16
store ptr %mask, ptr %mp
%cp = getelementptr i8, ptr %ii, i64 24
store ptr %color, ptr %cp
%icon = call ptr @CreateIconIndirect(ptr %ii)
%d1 = call i32 @DeleteObject(ptr %mask)
%d2 = call i32 @DeleteObject(ptr %color)
%noicon = icmp eq ptr %icon, null
br i1 %noicon, label %freepx, label %keep
keep:
store ptr %icon, ptr @W_icon
call void @w_apply_icon()
br label %freepx
freepx:
call void @free(ptr %px)
br label %out
out:
ret void
}