# ============================================================================ # gpu.ludic — the seam between the renderer and a graphics API. # # render3d was written straight against OpenGL, so there was nowhere to put a second # API. This file is where that stops: the renderer asks for what it wants here, and # the backend decides how to say it. OpenGL is the default and the fallback; Vulkan # is the second backend (docs: maroon-lake docs/plan/37-vulkan.md). # # The port is incremental. What has moved behind the seam so far: # - the choice of backend (R3D_GFX=gl|vk, or gpu_request before r3d_init) # - the fixed-function render state: depth test/func/write, blending, face # culling, colour writes, alpha-to-coverage, depth bias, scissor # - uniforms: looked up by program and name (gpu_uniform), set by the u_* setters # - vertex data: meshes, their attribute layouts, instance and stream buffers, draws # - textures: creation, upload, sampler state, mipmaps, units, read-back, freeing # - render targets and passes: framebuffers, attachments, blits, viewports, clears, the screen # # Render state is cached. A pipeline API bakes this state into an object picked by # key; OpenGL gets the same effect by only telling the driver what changed. The # cache is only right if NOTHING else in the renderer touches that state, so no # gl_enable / gl_disable / gl_depth_func / gl_depth_mask / gl_blend_func / # gl_cull_face / gl_color_mask / gl_scissor / gl_polygon_offset may appear outside # this file, and neither may any gl_uniform* call. # ============================================================================ const GPU_VK: int = 2 # Vulkan is the renderer (OpenGL was retired: maroon-lake docs/plan/22). gpu_request stays so a # caller that asks for a backend by name still builds; whatever it names, Vulkan is what runs. function gpu_request(render3d_st: mut Render3dState, name: string) -> void { render3d_st.gpu_wanted = GPU_VK } function gpu_name(kind: int) -> string { return "vulkan" } # Start Vulkan. There is nothing to fall back to: a machine whose Vulkan cannot start is told why # (gpu_fallback_reason, printed) and r3d_init fails, so the program can say so and stop. @alloc_ok("start-up: the device, its tables, the programs, the passes and the world's first textures are made once, before play") function gpu_select(render3d_st: mut Render3dState) -> int { if render3d_st.gpu_kind != 0 { return render3d_st.gpu_kind } # a window needs a surface: Win32 on Windows, a CAMetalLayer through MoltenVK on macOS render3d_st.gvk_want_surface = is_windowed() let os = Os.platform() if is_windowed() and os != "windows" and os != "macos" { render3d_st.gpu_fallback_reason = `no Vulkan window is built for {os}` } else if not gvk_init(render3d_st) { render3d_st.gpu_fallback_reason = render3d_st.gvk_why } else if not gvk_manifest(render3d_st) { render3d_st.gpu_fallback_reason = "the renderer's SPIR-V manifest is missing" } else { render3d_st.gpu_kind = GPU_VK } if render3d_st.gpu_kind != GPU_VK { gpu_fault_set(render3d_st, `vulkan cannot start: {render3d_st.gpu_fallback_reason}`) } return render3d_st.gpu_kind } function gpu_backend(render3d_st: Render3dState) -> string { return "vulkan" } # ---- render state ---------------------------------------------------------------- # -1 = not known yet: the first set always reaches the driver, so the cache never # assumes a default the context might not have. function gpu_b(on: bool) -> int { if on { return 1 }; return 0 } # Forget the cache: after anything outside the renderer may have changed GL state # (a context rebuilt, a foreign library drawing into the frame). function gpu_state_forget(render3d_st: mut Render3dState) -> void { render3d_st.gpu_s_depth_test = -1; render3d_st.gpu_s_depth_func = -1; render3d_st.gpu_s_depth_write = -1 render3d_st.gpu_s_blend = -1; render3d_st.gpu_s_blend_src = -1; render3d_st.gpu_s_blend_dst = -1 render3d_st.gpu_s_cull = -1; render3d_st.gpu_s_cull_face = -1; render3d_st.gpu_s_color_write = -1; render3d_st.gpu_s_a2c = -1 } function gpu_depth_test(render3d_st: mut Render3dState, on: bool) -> void { let v = gpu_b(on) if v == render3d_st.gpu_s_depth_test { return } render3d_st.gpu_s_depth_test = v } # GL_LESS, GL_LEQUAL, GL_EQUAL, GL_ALWAYS, ... (the comparison names are the same in every API) function gpu_depth_func(render3d_st: mut Render3dState, f: int) -> void { if f == render3d_st.gpu_s_depth_func { return } render3d_st.gpu_s_depth_func = f } function gpu_depth_write(render3d_st: mut Render3dState, on: bool) -> void { let v = gpu_b(on) if v == render3d_st.gpu_s_depth_write { return } render3d_st.gpu_s_depth_write = v } function gpu_blend(render3d_st: mut Render3dState, on: bool) -> void { let v = gpu_b(on) if v == render3d_st.gpu_s_blend { return } render3d_st.gpu_s_blend = v } function gpu_blend_func(render3d_st: mut Render3dState, src: int, dst: int) -> void { if src == render3d_st.gpu_s_blend_src and dst == render3d_st.gpu_s_blend_dst { return } render3d_st.gpu_s_blend_src = src; render3d_st.gpu_s_blend_dst = dst } function gpu_cull(render3d_st: mut Render3dState, on: bool) -> void { let v = gpu_b(on) if v == render3d_st.gpu_s_cull { return } render3d_st.gpu_s_cull = v } # GL_BACK or GL_FRONT function gpu_cull_face(render3d_st: mut Render3dState, face: int) -> void { if face == render3d_st.gpu_s_cull_face { return } render3d_st.gpu_s_cull_face = face } # all four channels together: nothing in the renderer writes a partial mask function gpu_color_write(render3d_st: mut Render3dState, on: bool) -> void { let v = gpu_b(on) if v == render3d_st.gpu_s_color_write { return } render3d_st.gpu_s_color_write = v } function gpu_alpha_to_coverage(render3d_st: mut Render3dState, on: bool) -> void { let v = gpu_b(on) if v == render3d_st.gpu_s_a2c { return } render3d_st.gpu_s_a2c = v } # Depth bias for the shadow casters; (0, 0) turns it off. factor/units are fixed, as # gl_polygon_offset takes them. A pipeline API bakes the bias into the pipeline. function gpu_depth_bias(render3d_st: mut Render3dState, factor: fixed, units: fixed) -> void { var v = 1 if factor == 0.0 and units == 0.0 { v = 0 } if v != render3d_st.gpu_s_bias { render3d_st.gpu_s_bias = v } render3d_st.gpu_s_bias_f = float(factor); render3d_st.gpu_s_bias_u = float(units) } # A scissor rectangle in top-down pixels (x, y from the top-left of the drawable), or # off. Every API but OpenGL counts rows from the top; the GL backend flips it. function gpu_scissor(render3d_st: mut Render3dState, x: int, y_top: int, w: int, h: int) -> void { render3d_st.gpu_s_scissor = 1; gvk_scissor(render3d_st, x, gl_height() - y_top - h, w, h) } function gpu_scissor_off(render3d_st: mut Render3dState) -> void { render3d_st.gpu_s_scissor = 0; gvk_scissor_off(render3d_st) } # ---- uniforms -------------------------------------------------------------------- # A uniform is found by its program and its name, and set through a handle. On OpenGL # the handle is the location. On a pipeline API it will name a slot in the program's # uniform block, so the u_* setters below are the only code that knows which. Arrays # are looked up by their first element ("u_bones[0]"), as every GL driver accepts. function gpu_uniform(render3d_st: Render3dState, prog: int, name: string) -> int { return gvk_uniform(render3d_st, prog, name) } # ---- programs ---------------------------------------------------------------------- # A program remembers the variant it was built from - vertex file, fragment file and the # defines on one line, the key the SPIR-V manifest uses - which is how a backend that cannot # compile shaders at run time finds its pipeline for the same handle. function gpu_program(render3d_st: mut Render3dState, vs: string, fs: string, defines: string) -> int { return gvk_program_new(render3d_st, vs, fs, defines) } # the manifest key a program was built from; "" for one this layer did not build function gpu_program_key(render3d_st: Render3dState, p: int) -> string { if render3d_st.gpu_prog_ids == null { return "" } for i in 0 .. len(render3d_st.gpu_prog_ids) { if render3d_st.gpu_prog_ids[i] == p { return render3d_st.gpu_prog_keys[i] } } return "" } function gpu_use_program(render3d_st: mut Render3dState, p: int) -> void { ds_program_change(render3d_st, render3d_st.gpu_prog_cur, p); render3d_st.gpu_prog_cur = p } function gpu_program_free(render3d_st: mut Render3dState, p: int) -> void { } # ---- GPU timers (R3D_PROF) ---------------------------------------------------------- function gpu_query_new(render3d_st: mut Render3dState, n: int, ids: words) -> void { gvk_query_new(render3d_st, n, ids) } function gpu_query_begin(render3d_st: mut Render3dState, id: int) -> void { gvk_query_begin(render3d_st, id) } function gpu_query_end(render3d_st: mut Render3dState) -> void { gvk_query_end(render3d_st) } # true once the query has its result; the nanoseconds (low 32 bits) are then in out[0] function gpu_query_result(render3d_st: mut Render3dState, id: int, out: words) -> bool { return gvk_query_result(render3d_st, id, out) } # ---- the context -------------------------------------------------------------------- function gpu_open(render3d_st: mut Render3dState, w: int, h: int, title: string) -> bool { return gvk_open(render3d_st, w, h, title) } function gpu_vsync(render3d_st: mut Render3dState, on: int) -> void { render3d_st.gvk_vsync = on != 0; if render3d_st.gvk_swap != 0 { render3d_st.gvk_swap_stale = true } } function gpu_renderer_name(render3d_st: Render3dState) -> string { return render3d_st.gvk_renderer_name } function gpu_resize_check(render3d_st: mut Render3dState) -> bool { return gvk_resize_check(render3d_st) } # the finished frame: presented to the window, or (headless) the GPU's work finished function gpu_present(render3d_st: mut Render3dState) -> void { gvk_present(render3d_st) } # the frame as it will be presented, to a binary PPM with the top row first; before gpu_present function gpu_screenshot(render3d_st: mut Render3dState, path: string) -> bool { return gvk_screenshot(render3d_st, path) } function gpu_tmp(render3d_st: mut Render3dState) -> words { return render3d_st.gpu_u_tmp } # float bits (IEEE singles in an int), like every other number in the renderer function u_f(render3d_st: mut Render3dState, loc: int, v: float) -> void { let t = gpu_tmp(render3d_st); t[0] = float_bits(v); gvk_u_set(render3d_st, loc, data_of(t), 4, 1) } function u_f2(render3d_st: mut Render3dState, loc: int, x: float, y: float) -> void { let t = gpu_tmp(render3d_st); t[0] = float_bits(x); t[1] = float_bits(y); gvk_u_set(render3d_st, loc, data_of(t), 8, 1) } function u_f3(render3d_st: mut Render3dState, loc: int, x: float, y: float, z: float) -> void { let t = gpu_tmp(render3d_st); t[0] = float_bits(x); t[1] = float_bits(y); t[2] = float_bits(z); gvk_u_set(render3d_st, loc, data_of(t), 12, 1) } function u_f4(render3d_st: mut Render3dState, loc: int, x: float, y: float, z: float, w: float) -> void { let t = gpu_tmp(render3d_st); t[0] = float_bits(x); t[1] = float_bits(y); t[2] = float_bits(z); t[3] = float_bits(w); gvk_u_set(render3d_st, loc, data_of(t), 16, 1) } function u_v3(render3d_st: mut Render3dState, loc: int, v: floats) -> void { gvk_u_set(render3d_st, loc, data_of(v), 12, 1) } function u_fv(render3d_st: mut Render3dState, loc: int, n: int, v: floats) -> void { gvk_u_set(render3d_st, loc, data_of(v), 4, n) } # n vec4s from 4n float bits. Not u_fv with 4n: on Vulkan an array element is copied at the size # given and placed at the array's stride, so a vec4 array fed floats got one float per element - # which drew the chunked grass with every tile at a nonsense corner and zero blades a cell. function u_f4v(render3d_st: mut Render3dState, loc: int, n: int, v: floats) -> void { gvk_u_set(render3d_st, loc, data_of(v), 16, n) } function u_mat4(render3d_st: mut Render3dState, loc: int, m: floats) -> void { gvk_u_set(render3d_st, loc, data_of(m), 64, 1) } function u_mat4n(render3d_st: mut Render3dState, loc: int, n: int, m: floats) -> void { gvk_u_set(render3d_st, loc, data_of(m), 64, n) } function u_i(render3d_st: mut Render3dState, loc: int, v: int) -> void { let t = gpu_tmp(render3d_st); t[0] = v; gvk_u_set(render3d_st, loc, data_of(t), 4, 1) } # ---- what this machine can do ---------------------------------------------------- # The advanced graphics features are Windows features: the Vulkan renderer, ray tracing, # DLSS, Reflex, HDR output, mesh-shader ground cover. gpu_caps_probe() asks Vulkan what # the GPU offers - on Windows only - and a game's settings screen greys out whatever this # machine cannot use, with the most specific reason. Detected every start, never saved: # a settings file carried to another machine must not carry a stale "supported". const GF_VULKAN: int = 0 const GF_RT_SHADOWS: int = 1 const GF_RT_REFLECTIONS: int = 2 const GF_RT_AO: int = 3 const GF_DLSS: int = 4 const GF_DLSS_RR: int = 5 const GF_DLSS_FG: int = 6 const GF_DLSS5: int = 7 const GF_REFLEX: int = 8 const GF_HDR: int = 9 const GF_MESH_GRASS: int = 10 const GF_COUNT: int = 11 # 20 for "NVIDIA GeForce RTX 2080", 50 for "RTX 5090"; 30 for a workstation "RTX A4000" function gpu_rtx_generation(name: string) -> int { let at = Text.index_of(name, "RTX ") if at < 0 { return 0 } let p: pointer = name let c = p[at + 4] if c >= '0' and c <= '9' { return (c - '0') * 10 } return 30 } function gpu_ext_in(props: bytes, n: int, want: string) -> bool { for i in 0 .. n { if string(Vk.at(props, i * VkExtensionProperties_sizeof + VkExtensionProperties_extensionName)) == want { return true } } return false } # R3D_CAPS=rtx50|rtx40|rtx30|amd|intel|none pretends to be a Windows machine with that GPU, # so the settings screen can be shot and tested anywhere @alloc_ok("a developer switch (R3D_CAPS): the caps faked once at start-up") function gpu_caps_fake(render3d_st: mut Render3dState, kind: string) -> void { render3d_st.gpu_cap_windows = true if kind == "none" { return } render3d_st.gpu_cap_vulkan = true; render3d_st.gpu_cap_floor = true; render3d_st.gpu_cap_hdr = true if kind == "amd" or kind == "intel" { render3d_st.gpu_cap_rt = true; render3d_st.gpu_cap_mesh = true; render3d_st.gpu_cap_device = `test {kind} GPU`; return } render3d_st.gpu_cap_nvidia = true; render3d_st.gpu_cap_rt = true; render3d_st.gpu_cap_mesh = true; render3d_st.gpu_cap_reflex = true let rtx = `RTX {kind[3 .. 5]}` render3d_st.gpu_cap_rtx = gpu_rtx_generation(rtx) free(rtx) render3d_st.gpu_cap_device = `test NVIDIA GeForce RTX {kind[3 .. 5]}` } @alloc_ok("start-up: the machine's capabilities, probed once (or on a settings page asking)") function gpu_caps_probe(render3d_st: mut Render3dState) -> void { if render3d_st.gpu_cap_probed { return } render3d_st.gpu_cap_probed = true if r3d_env_has(render3d_st, "R3D_CAPS") { gpu_caps_fake(render3d_st, r3d_env(render3d_st, "R3D_CAPS")); return } render3d_st.gpu_cap_windows = Os.platform() == "windows" if not render3d_st.gpu_cap_windows { return } if Vk.open() == 0 { return } let cnt = bytes(4) Vk.put_i32(cnt, 0, 0) Vk.enumerate_instance_extension_properties(null, cnt, null) let nie = Vk.get_i32(cnt, 0) let iexts = bytes(nie * VkExtensionProperties_sizeof + 8) Vk.enumerate_instance_extension_properties(null, cnt, iexts) render3d_st.gpu_cap_hdr = gpu_ext_in(iexts, nie, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME) let app = bytes(VkApplicationInfo_sizeof) Vk.zero(app, VkApplicationInfo_sizeof) Vk.put_i32(app, VkApplicationInfo_sType, VK_STRUCTURE_TYPE_APPLICATION_INFO) Vk.put_i32(app, VkApplicationInfo_apiVersion, (1 << 22) | (3 << 12)) let ici = bytes(VkInstanceCreateInfo_sizeof) Vk.zero(ici, VkInstanceCreateInfo_sizeof) Vk.put_i32(ici, VkInstanceCreateInfo_sType, VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO) Vk.put_ptr(ici, VkInstanceCreateInfo_pApplicationInfo, app) # With the Vulkan renderer running, ask its own instance: the renderer's loader is NVIDIA Streamline's # interposer, and a second instance made and destroyed under it is what this avoids. var inst: pointer = null var own = false if render3d_st.gvk_ready { inst = render3d_st.gvk_inst } else { let out = bytes(8) let made = Vk.create_instance(ici, null, out) free(app); free(ici) if made != VK_SUCCESS { free(out) return } inst = Vk.get_ptr(out, 0) free(out) own = true } Vk.put_i32(cnt, 0, 0) Vk.enumerate_physical_devices(inst, cnt, null) let nd = Vk.get_i32(cnt, 0) let devs = bytes(nd * 8 + 8) Vk.enumerate_physical_devices(inst, cnt, devs) let props = bytes(VkPhysicalDeviceProperties_sizeof) # the renderer runs on the first discrete GPU, else the first one listed var pick = -1 for d in 0 .. nd { Vk.get_physical_device_properties(Vk.get_ptr(devs, d * 8), props) if pick < 0 and Vk.get_i32(props, VkPhysicalDeviceProperties_deviceType) == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU { pick = d } } if pick < 0 and nd > 0 { pick = 0 } if pick >= 0 { let pd = Vk.get_ptr(devs, pick * 8) render3d_st.gpu_cap_vulkan = true Vk.get_physical_device_properties(pd, props) render3d_st.gpu_cap_device = string(Vk.at(props, VkPhysicalDeviceProperties_deviceName)) render3d_st.gpu_cap_nvidia = Vk.get_i32(props, VkPhysicalDeviceProperties_vendorID) == 4318 if render3d_st.gpu_cap_nvidia { render3d_st.gpu_cap_rtx = gpu_rtx_generation(render3d_st.gpu_cap_device) } let api = Vk.get_i32(props, VkPhysicalDeviceProperties_apiVersion) let f13 = bytes(VkPhysicalDeviceVulkan13Features_sizeof) Vk.zero(f13, VkPhysicalDeviceVulkan13Features_sizeof) Vk.put_i32(f13, VkPhysicalDeviceVulkan13Features_sType, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES) let f12 = bytes(VkPhysicalDeviceVulkan12Features_sizeof) Vk.zero(f12, VkPhysicalDeviceVulkan12Features_sizeof) Vk.put_i32(f12, VkPhysicalDeviceVulkan12Features_sType, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES) Vk.put_ptr(f12, VkPhysicalDeviceVulkan12Features_pNext, f13) let f2 = bytes(VkPhysicalDeviceFeatures2_sizeof) Vk.zero(f2, VkPhysicalDeviceFeatures2_sizeof) Vk.put_i32(f2, VkPhysicalDeviceFeatures2_sType, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2) Vk.put_ptr(f2, VkPhysicalDeviceFeatures2_pNext, f12) Vk.get_physical_device_features2(pd, f2) let major = (api >> 22) & 127 let minor = (api >> 12) & 1023 render3d_st.gpu_cap_floor = (major > 1 or (major == 1 and minor >= 3)) and Vk.get_i32(f13, VkPhysicalDeviceVulkan13Features_dynamicRendering) == 1 and Vk.get_i32(f13, VkPhysicalDeviceVulkan13Features_synchronization2) == 1 and Vk.get_i32(f12, VkPhysicalDeviceVulkan12Features_descriptorIndexing) == 1 and Vk.get_i32(f12, VkPhysicalDeviceVulkan12Features_bufferDeviceAddress) == 1 and Vk.get_i32(f12, VkPhysicalDeviceVulkan12Features_timelineSemaphore) == 1 Vk.put_i32(cnt, 0, 0) Vk.enumerate_device_extension_properties(pd, null, cnt, null) let ne = Vk.get_i32(cnt, 0) let dexts = bytes(ne * VkExtensionProperties_sizeof + 8) Vk.enumerate_device_extension_properties(pd, null, cnt, dexts) render3d_st.gpu_cap_rt = gpu_ext_in(dexts, ne, VK_KHR_RAY_QUERY_EXTENSION_NAME) and gpu_ext_in(dexts, ne, VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME) render3d_st.gpu_cap_mesh = gpu_ext_in(dexts, ne, VK_EXT_MESH_SHADER_EXTENSION_NAME) render3d_st.gpu_cap_reflex = gpu_ext_in(dexts, ne, VK_NV_LOW_LATENCY_2_EXTENSION_NAME) } if own { Vk.destroy_instance(inst, null) } print(`r3d: gpu caps: {render3d_st.gpu_cap_device} vulkan {render3d_st.gpu_cap_vulkan} floor {render3d_st.gpu_cap_floor} rt {render3d_st.gpu_cap_rt} mesh {render3d_st.gpu_cap_mesh} rtx {render3d_st.gpu_cap_rtx} reflex {render3d_st.gpu_cap_reflex} hdr {render3d_st.gpu_cap_hdr}`) } # Whether the renderer actually draws a feature yet. Until a feature lands, choosing it is saved and # shown, and says it takes effect later. The Vulkan renderer draws the whole game (phase 37-38), and # DLSS super resolution and Reflex run through NVIDIA Streamline (streamline.ludic), and HDR output is an # HDR10 swapchain (gpu_vk_draw.ludic). Mesh-shader grass draws but is not yet counted: at 4K on an # RTX 3070 Ti its grass pass took 5.0 ms against the chunked path's 1.7. Whether this # machine can use one is the caps' question, not this one. function gpu_feature_implemented(f: int) -> bool { return f == GF_VULKAN or f == GF_DLSS or f == GF_REFLEX or f == GF_HDR } # ---- vertex data -------------------------------------------------------------------- # A Mesh is built through these and records what it is made of - which buffer feeds which # attribute, at what stride and offset, per vertex or per instance - so a backend that bakes # vertex input into a pipeline (Vulkan) can read the layout back. On OpenGL each call is the # GL it replaces, in the same order: a vertex array object per mesh, bound while it is built. const GPU_F32: int = 1 const GPU_U8: int = 2 const GPU_U16: int = 3 const GPU_STATIC: int = 0 const GPU_DYNAMIC: int = 1 const GPU_STREAM: int = 2 const GPU_MAX_ATTRS: int = 8 const GPU_ATTR_W: int = 7 # per attribute index: buffer, comps, type, stride, offset, normalized, per instance const GPU_MAX_VBUFS: int = 8 function gpu_gl_type(t: int) -> int { if t == GPU_U8 { return GL_UNSIGNED_BYTE } if t == GPU_U16 { return GL_UNSIGNED_SHORT } return GL_FLOAT } function gpu_type_bytes(t: int) -> int { if t == GPU_U8 { return 1 } if t == GPU_U16 { return 2 } return 4 } function gpu_gl_usage(u: int) -> int { if u == GPU_DYNAMIC { return GL_DYNAMIC_DRAW } if u == GPU_STREAM { return GL_STREAM_DRAW } return GL_STATIC_DRAW } # a new mesh, its vertex array bound: the vertex, attribute and index calls below describe it @alloc_ok("loading a model, a texture or a font: a load, not a frame (a guest loading a teammate's look is one)") @creates(Mesh) function gpu_mesh_new(render3d_st: Render3dState) -> Mesh { let m = new Mesh m.attrs = words(GPU_MAX_ATTRS * GPU_ATTR_W) for i in 0 .. GPU_MAX_ATTRS * GPU_ATTR_W { m.attrs[i] = 0 } m.vbufs = words(GPU_MAX_VBUFS) return m } # a vertex buffer for the mesh being built (data may be null: storage only); returns it function gpu_mesh_vertices(render3d_st: mut Render3dState, m: Mesh, data: pointer, nbytes: int, usage: int) -> int { var b = 0 b = gvk_buf_new(render3d_st); gvk_buf_upload(render3d_st, b, nbytes, data) if m.vbo == 0 { m.vbo = b } if m.n_vbufs < GPU_MAX_VBUFS { m.vbufs[m.n_vbufs] = b; m.n_vbufs += 1 } m.cur_buf = b return b } function gpu_mesh_record(m: Mesh, index: int, comps: int, type: int, stride: int, offset: int, normalized: bool, inst: bool) -> void { if index < 0 or index >= GPU_MAX_ATTRS { return } let o = index * GPU_ATTR_W var st = stride if st == 0 { st = comps * gpu_type_bytes(type) } # re-pointing an attribute at another buffer keeps the layout; changing its shape does not if m.attrs[o + 1] != comps or m.attrs[o + 2] != type or m.attrs[o + 3] != st or m.attrs[o + 4] != offset or m.attrs[o + 5] != gpu_b(normalized) or m.attrs[o + 6] != gpu_b(inst) or (m.attrs[o] == m.attrs[0]) != (m.cur_buf == m.attrs[0]) { m.vk_layout = 0 } m.attrs[o] = m.cur_buf; m.attrs[o + 1] = comps; m.attrs[o + 2] = type; m.attrs[o + 3] = st m.attrs[o + 4] = offset; m.attrs[o + 5] = gpu_b(normalized); m.attrs[o + 6] = gpu_b(inst) if index + 1 > m.n_attrs { m.n_attrs = index + 1 } } # attribute `index` read from the last vertex buffer (stride 0: tightly packed) function gpu_mesh_attr(render3d_st: Render3dState, m: Mesh, index: int, comps: int, type: int, stride: int, offset: int, normalized: bool) -> void { gpu_mesh_record(m, index, comps, type, stride, offset, normalized, false) } # the index buffer: 4-byte or 2-byte indices function gpu_mesh_indices(render3d_st: mut Render3dState, m: Mesh, data: pointer, nbytes: int, index_bytes: int) -> void { m.itype = GL_UNSIGNED_INT if index_bytes == 2 { m.itype = GL_UNSIGNED_SHORT } m.ebo = gvk_buf_new(render3d_st); gvk_buf_upload(render3d_st, m.ebo, nbytes, data) } # finished describing: nothing else is bound to it by accident function gpu_mesh_done(render3d_st: Render3dState, m: Mesh) -> void { } # Per-instance data: `buf` feeds the attributes named next, one element per instance. A mesh # drawn from different instance buffers (the scatter layers' LOD buckets) is re-pointed here # before each draw; on Vulkan that is a vertex-buffer binding, not a change of layout. function gpu_mesh_bind_instances(render3d_st: Render3dState, m: Mesh, buf: int) -> void { m.cur_buf = buf m.ibuf = buf } function gpu_mesh_attr_inst(render3d_st: Render3dState, m: Mesh, index: int, comps: int, type: int, stride: int, offset: int) -> void { gpu_mesh_record(m, index, comps, type, stride, offset, false, true) } # a buffer on its own (instances, a stream): made, filled whole, freed @creates(GpuBuffer) function gpu_buffer_new(render3d_st: mut Render3dState) -> int { return gvk_buf_new(render3d_st) } function gpu_buffer_upload(render3d_st: mut Render3dState, buf: int, nbytes: int, data: pointer, usage: int) -> void { gvk_buf_upload(render3d_st, buf, nbytes, data) } @releases(GpuBuffer) function gpu_buffer_free(render3d_st: mut Render3dState, buf: int) -> void { if buf > 0 { gvk_buf_release(render3d_st, buf) } } # ---- compute and indirect draws (Vulkan) ---------------------------------------------------- # The GPU-driven path: a compute program writes instance lists and draw commands into buffers # the draws then read. OpenGL here is 4.1 (macOS) with no compute, so gpu_compute is 0 there and # a caller keeps its CPU path. A compute program's binding 0 is its parameter block (params, # copied at the dispatch); bindings 1.. are `bufs`, gpu buffers. function gpu_has_compute(render3d_st: Render3dState) -> bool { return true } # several records in one indirect draw, each with its own firstInstance function gpu_has_mdi(render3d_st: Render3dState) -> bool { return render3d_st.gvk_has_mdi } # mesh shaders (VK_EXT_mesh_shader): a *.mesh program drawn with gpu_draw_mesh_tasks function gpu_has_mesh(render3d_st: Render3dState) -> bool { return render3d_st.gvk_has_mesh } function gpu_draw_mesh_tasks(render3d_st: mut Render3dState, x: int, y: int, z: int) -> void { gvk_draw_mesh_tasks_now(render3d_st, x, y, z) } function gpu_compute(render3d_st: mut Render3dState, name: string, n_bufs: int) -> int { return gvk_compute_new(render3d_st, name, n_bufs) } function gpu_dispatch(render3d_st: mut Render3dState, c: int, params: pointer, n_params: int, bufs: words, groups: int) -> void { if c > 0 { gvk_dispatch(render3d_st, c, params, n_params, bufs, groups, 1, 1) } } # a compute program that also samples n_tex textures (bound after its buffers), and its 2-D dispatch function gpu_compute_tex(render3d_st: mut Render3dState, name: string, n_bufs: int, n_tex: int) -> int { return gvk_compute_new_tex(render3d_st, name, n_bufs, n_tex) } function gpu_dispatch_tex(render3d_st: mut Render3dState, c: int, params: pointer, n_params: int, bufs: words, texs: words, gx: int, gy: int) -> void { if c > 0 { gvk_dispatch_tex(render3d_st, c, params, n_params, bufs, texs, gx, gy, 1) } } # a buffer a compute pass writes (never reallocated under a draw that reads it) function gpu_buffer_gpu_owned(render3d_st: mut Render3dState, buf: int) -> void { gvk_buf_gpu_owned(render3d_st, buf) } # the host-visible contents of a buffer, for a readback after gpu_finish; null on OpenGL function gpu_buffer_map(render3d_st: Render3dState, buf: int) -> pointer { if buf <= 0 { return null } return render3d_st.gvk_buf_map[buf] } function gpu_finish(render3d_st: mut Render3dState) -> void { gvk_flush(render3d_st) } # n indexed draws of mesh m from VkDrawIndexedIndirectCommand records in buffer cmds at offset # (bytes); each record's firstInstance selects its instances out of the bound instance buffer. # With count_buf > 0 the GPU's own count (a uint at count_off) is used, up to n. function gpu_draw_mesh_indirect(render3d_st: mut Render3dState, m: Mesh, cmds: int, offset: int, n: int, count_buf: int, count_off: int) -> void { if m != null { ds_draw(render3d_st, DS_INDIRECT, n, m.count) } gvk_draw_indirect_now(render3d_st, m, cmds, offset, n, count_buf, count_off) } # drawing function gpu_mesh_bind(render3d_st: Render3dState, m: Mesh) -> void { } function gpu_mesh_unbind(render3d_st: Render3dState) -> void { } function gpu_draw_mesh(render3d_st: mut Render3dState, m: Mesh) -> void { ds_draw(render3d_st, DS_MESH, 1, m.count) gvk_draw_now(render3d_st, m, 0, 0, 1) } function gpu_draw_mesh_instanced(render3d_st: mut Render3dState, m: Mesh, n: int) -> void { ds_draw(render3d_st, DS_INSTANCED, n, m.count * n) gvk_draw_now(render3d_st, m, 0, 0, n) } # the bound mesh's indices again (a patch mesh drawn once per terrain node) function gpu_draw_bound_elements(render3d_st: mut Render3dState, m: Mesh) -> void { ds_draw(render3d_st, DS_PATCH, 1, m.count) gvk_draw_now(render3d_st, m, 0, 0, 1) } # vertices [first, first + count) of the bound mesh, as triangles (the overlay's ranges) function gpu_draw_range(render3d_st: mut Render3dState, m: Mesh, first: int, count: int) -> void { ds_draw(render3d_st, DS_RANGE, 1, count) gvk_draw_now(render3d_st, m, first, count, 1) } @releases(Mesh) function gpu_mesh_free(render3d_st: mut Render3dState, m: Mesh) -> void { gvk_mesh_free(render3d_st, m) } # ---- textures ----------------------------------------------------------------------------- # A texture is a handle (on OpenGL, the texture name). Each call below is the one GL call it # replaces, in the renderer's own order, so OpenGL draws exactly what it drew. What those calls # say about a texture - its size, format, filters, wraps, comparison, mipmaps, anisotropy - is # recorded per handle as it is set, because a backend with immutable images and separate # sampler objects (Vulkan) creates both from exactly that. Parameters apply to the texture last # bound for that kind, which is how every call site here already works. const GPU_TEX2D: int = 1 const GPU_TEX2D_ARRAY: int = 2 const GPU_TX_W: int = 12 # per handle: kind, w, h, layers, ifmt, min, mag, wrap s, wrap t, compare, mips, aniso function gpu_gl_target(kind: int) -> int { if kind == GPU_TEX2D_ARRAY { return GL_TEXTURE_2D_ARRAY }; return GL_TEXTURE_2D } # the record for a handle, growing the table when a new name is larger than it @alloc_ok("a texture handle past the 4096 made at start-up: the table doubles to hold it (ids are reused, so it stops)") function gpu_tx_at(render3d_st: mut Render3dState, tex: int) -> int { if tex <= 0 { return -1 } if tex >= render3d_st.gpu_tx_cap { var cap = render3d_st.gpu_tx_cap * 2 if cap < 256 { cap = 256 } while cap <= tex { cap = cap * 2 } let t = words(cap * GPU_TX_W) for i in 0 .. cap * GPU_TX_W { t[i] = 0 } if render3d_st.gpu_tx != null { mem_copy(t, render3d_st.gpu_tx, render3d_st.gpu_tx_cap * GPU_TX_W * 4); free(render3d_st.gpu_tx) } render3d_st.gpu_tx = t render3d_st.gpu_tx_cap = cap } return tex * GPU_TX_W } function gpu_bound(render3d_st: Render3dState, kind: int) -> int { if kind == GPU_TEX2D_ARRAY { return render3d_st.gpu_bound_array }; return render3d_st.gpu_bound_2d } @creates(GpuTexture) function gpu_tex_new(render3d_st: mut Render3dState) -> int { return gvk_tex_new(render3d_st) } # does texture `tex` have an image behind it? Always on OpenGL; on Vulkan an image whose memory could # not be had is never made, and a caller that can fall back (a smaller shadow map) asks here. function gpu_tex_ok(render3d_st: Render3dState, tex: int) -> bool { return tex > 0 and tex < len(render3d_st.gvk_tex_image) and render3d_st.gvk_tex_image[tex] != 0 } # what GL has on each unit's 2D target, for R3D_GLCHECK: deleting a texture unbinds it everywhere function gpu_tex_unit(render3d_st: mut Render3dState, unit: int) -> void { render3d_st.gpu_unit_cur = unit } function gpu_tex_bind(render3d_st: mut Render3dState, kind: int, tex: int) -> void { ds_tex_bind(render3d_st) if kind == GPU_TEX2D_ARRAY { render3d_st.gpu_bound_array = tex } else { render3d_st.gpu_bound_2d = tex } if kind != GPU_TEX2D_ARRAY and render3d_st.gpu_unit_cur < 32 { render3d_st.gpu_unit_2d[render3d_st.gpu_unit_cur] = tex } } # pixel transfer packing (alignment, byte swap) for the uploads and read-backs that follow function gpu_pixel_store(render3d_st: mut Render3dState, pname: int, value: int) -> void { if pname == GL_UNPACK_SWAP_BYTES { render3d_st.gvk_unpack_swap = value == 1 } } function gpu_tex_image2d(render3d_st: mut Render3dState, ifmt: int, w: int, h: int, fmt: int, ty: int, data: pointer) -> void { gvk_flush(render3d_st) gvk_tex_storage(render3d_st, render3d_st.gpu_bound_2d, false, ifmt, w, h, 1, data != null) if data != null { gvk_tex_upload(render3d_st, render3d_st.gpu_bound_2d, ifmt, w, h, 1, fmt, ty, data) } let o = gpu_tx_at(render3d_st, render3d_st.gpu_bound_2d) if o >= 0 { render3d_st.gpu_tx[o] = GPU_TEX2D; render3d_st.gpu_tx[o + 1] = w; render3d_st.gpu_tx[o + 2] = h; render3d_st.gpu_tx[o + 3] = 1; render3d_st.gpu_tx[o + 4] = ifmt } } function gpu_tex_image3d(render3d_st: mut Render3dState, ifmt: int, w: int, h: int, layers: int, fmt: int, ty: int, data: pointer) -> void { gvk_flush(render3d_st) gvk_tex_storage(render3d_st, render3d_st.gpu_bound_array, true, ifmt, w, h, layers, data != null) if data != null { gvk_tex_upload(render3d_st, render3d_st.gpu_bound_array, ifmt, w, h, layers, fmt, ty, data) } let o = gpu_tx_at(render3d_st, render3d_st.gpu_bound_array) if o >= 0 { render3d_st.gpu_tx[o] = GPU_TEX2D_ARRAY; render3d_st.gpu_tx[o + 1] = w; render3d_st.gpu_tx[o + 2] = h; render3d_st.gpu_tx[o + 3] = layers; render3d_st.gpu_tx[o + 4] = ifmt } } function gpu_tex_param(render3d_st: mut Render3dState, kind: int, pname: int, value: int) -> void { let o = gpu_tx_at(render3d_st, gpu_bound(render3d_st, kind)) if o < 0 { return } if pname == GL_TEXTURE_MIN_FILTER { render3d_st.gpu_tx[o + 5] = value } if pname == GL_TEXTURE_MAG_FILTER { render3d_st.gpu_tx[o + 6] = value } if pname == GL_TEXTURE_WRAP_S { render3d_st.gpu_tx[o + 7] = value } if pname == GL_TEXTURE_WRAP_T { render3d_st.gpu_tx[o + 8] = value } if pname == GL_TEXTURE_COMPARE_MODE { if value == GL_NONE { render3d_st.gpu_tx[o + 9] = 0 } } if pname == GL_TEXTURE_COMPARE_FUNC { render3d_st.gpu_tx[o + 9] = value } } # a float parameter (fixed, as Gl.* takes it): anisotropy is the one the renderer sets function gpu_tex_paramf(render3d_st: mut Render3dState, kind: int, pname: int, value: fixed) -> void { let o = gpu_tx_at(render3d_st, gpu_bound(render3d_st, kind)) if o >= 0 and pname == 0x84FE { render3d_st.gpu_tx[o + 11] = float_bits(float(value)) } } # the border colour clamp-to-border reads (four fixed values in `rgba`) function gpu_tex_border(render3d_st: Render3dState, kind: int, rgba: pointer) -> void { } # a block-compressed texture with its whole mip chain, into the bound 2D texture: levels as they # are in the file (level l at offs[l]), no decoding on the CPU and no mips made here function gpu_tex_compressed(render3d_st: mut Render3dState, ifmt: int, w: int, h: int, levels: int, data: pointer, offs: words) -> bool { gvk_flush(render3d_st) let tex = render3d_st.gpu_bound_2d if not gvk_tex_storage(render3d_st, tex, false, ifmt, w, h, 1, true) { return false } if render3d_st.gvk_tex_levels[tex] != levels { print(`r3d: a {w}x{h} compressed texture brought {levels} levels, not {render3d_st.gvk_tex_levels[tex]}`); return false } if not gvk_tex_upload_blocks(render3d_st, tex, w, h, levels, data, offs) { return false } let o = gpu_tx_at(render3d_st, tex) if o >= 0 { render3d_st.gpu_tx[o] = GPU_TEX2D; render3d_st.gpu_tx[o + 1] = w; render3d_st.gpu_tx[o + 2] = h; render3d_st.gpu_tx[o + 3] = 1; render3d_st.gpu_tx[o + 4] = ifmt; render3d_st.gpu_tx[o + 10] = 1 } return true } function gpu_tex_mips(render3d_st: mut Render3dState, kind: int) -> void { let mt = gpu_bound(render3d_st, kind) let mo = gpu_tx_at(render3d_st, mt) if mo >= 0 { gvk_mips_now(render3d_st, mt, render3d_st.gpu_tx[mo + 1], render3d_st.gpu_tx[mo + 2]) } let o = gpu_tx_at(render3d_st, gpu_bound(render3d_st, kind)) if o >= 0 { render3d_st.gpu_tx[o + 10] = 1 } } # level 0 of the bound texture into `out` function gpu_tex_read(render3d_st: mut Render3dState, kind: int, fmt: int, ty: int, out: pointer) -> void { gvk_flush(render3d_st); let rt = gpu_bound(render3d_st, kind); let ro = gpu_tx_at(render3d_st, rt); if ro >= 0 { gvk_tex_read(render3d_st, rt, render3d_st.gpu_tx[ro + 4], render3d_st.gpu_tx[ro + 1], render3d_st.gpu_tx[ro + 2], fmt, ty, out) } } @releases(GpuTexture) function gpu_tex_free(render3d_st: mut Render3dState, tex: int) -> void { if tex == 0 { return } let ids = gpu_tmp(render3d_st) ids[0] = tex gvk_flush(render3d_st) gvk_tex_give_back(render3d_st, tex) tex_note_forget(render3d_st, tex) if render3d_st.gpu_unit_2d != null { for i in 0 .. 32 { if render3d_st.gpu_unit_2d[i] == tex { render3d_st.gpu_unit_2d[i] = 0 } } } if render3d_st.gpu_bound_2d == tex { render3d_st.gpu_bound_2d = 0 } let o = gpu_tx_at(render3d_st, tex) if o >= 0 { for i in 0 .. GPU_TX_W { render3d_st.gpu_tx[o + i] = 0 } } } # a texture on a unit for a program's sampler, by the sampler's name function gpu_bind_sampler(render3d_st: mut Render3dState, prog: int, name: string, unit: int, kind: int, tex: int) -> void { gvk_bind_texture(render3d_st, prog, name, tex) # as OpenGL's did, binding a texture for a sampler makes it the one a following gpu_tex_param or # gpu_tex_mips acts on: render3d was written that way, and without it they reached whatever was # bound last (a BC7 kit texture, which a mip blit then drew into) if kind == GPU_TEX2D_ARRAY { render3d_st.gpu_bound_array = tex } else { render3d_st.gpu_bound_2d = tex } } # ---- render targets and passes ------------------------------------------------------------- # A framebuffer is a handle (OpenGL's name). What is attached to it - colour textures in slots, # a depth texture or one layer of an array, renderbuffers and their sample count - is recorded as # it is attached, which is what a backend with render passes and image views builds from. Each # call is the GL call it replaces, in the renderer's order. # # gpu_check(tag) reports a pending error under a name, as gl_check did. R3D_GLCHECK=1 adds a # completeness check of the bound framebuffer whenever a viewport is set, naming the handle, so # a pass drawing into an incomplete target says which one; unset, it calls nothing. const GPU_FB_W: int = 8 # per handle: colour 0, colour 1, depth texture, depth layer + 1, colour rb, depth rb, samples, colour layer + 1 @alloc_ok("a framebuffer handle past the 1024 made at start-up: the table doubles to hold it (bounded by targets)") function gpu_fb_at(render3d_st: mut Render3dState, fb: int) -> int { if fb <= 0 { return -1 } if fb >= render3d_st.gpu_fb_cap { var cap = render3d_st.gpu_fb_cap * 2 if cap < 64 { cap = 64 } while cap <= fb { cap = cap * 2 } let t = words(cap * GPU_FB_W) for i in 0 .. cap * GPU_FB_W { t[i] = 0 } if render3d_st.gpu_fb != null { mem_copy(t, render3d_st.gpu_fb, render3d_st.gpu_fb_cap * GPU_FB_W * 4); free(render3d_st.gpu_fb) } render3d_st.gpu_fb = t render3d_st.gpu_fb_cap = cap } return fb * GPU_FB_W } function gpu_check(render3d_st: Render3dState, tag: string) -> int { return 0 } # a named checkpoint that costs nothing unless R3D_GLCHECK is set function gpu_debug_check(render3d_st: mut Render3dState, tag: string) -> void { } @creates(GpuFramebuffer) function gpu_fb_new(render3d_st: mut Render3dState) -> int { render3d_st.gvk_fb_counter += 1; return render3d_st.gvk_fb_counter } function gpu_fb_bind(render3d_st: mut Render3dState, fb: int) -> void { gvk_rebind(render3d_st, fb) render3d_st.gpu_fb_cur = fb } function gpu_fb_bind_read(render3d_st: mut Render3dState, fb: int) -> void { render3d_st.gvk_fb_read = fb } function gpu_fb_bind_draw(render3d_st: mut Render3dState, fb: int) -> void { render3d_st.gvk_fb_draw = fb } function gpu_fb_color(render3d_st: mut Render3dState, slot: int, tex: int) -> void { gvk_pass_end(render3d_st) let o = gpu_fb_at(render3d_st, render3d_st.gpu_fb_cur) if o >= 0 and slot < 2 { render3d_st.gpu_fb[o + slot] = tex; if slot == 0 { render3d_st.gpu_fb[o + 7] = 0 } } } function gpu_fb_color_layer(render3d_st: mut Render3dState, slot: int, tex: int, layer: int) -> void { gvk_pass_end(render3d_st) let o = gpu_fb_at(render3d_st, render3d_st.gpu_fb_cur) if o >= 0 and slot < 2 { render3d_st.gpu_fb[o + slot] = tex; if slot == 0 { render3d_st.gpu_fb[o + 7] = layer + 1 } } } function gpu_fb_depth(render3d_st: mut Render3dState, tex: int) -> void { gvk_pass_end(render3d_st) let o = gpu_fb_at(render3d_st, render3d_st.gpu_fb_cur) if o >= 0 { render3d_st.gpu_fb[o + 2] = tex; render3d_st.gpu_fb[o + 3] = 0 } } function gpu_fb_depth_layer(render3d_st: mut Render3dState, tex: int, layer: int) -> void { gvk_pass_end(render3d_st) let o = gpu_fb_at(render3d_st, render3d_st.gpu_fb_cur) if o >= 0 { render3d_st.gpu_fb[o + 2] = tex; render3d_st.gpu_fb[o + 3] = layer + 1 } } @creates(GpuRenderbuffer) function gpu_rb_new(render3d_st: mut Render3dState) -> int { return gvk_tex_new(render3d_st) } # storage for a renderbuffer: samples > 0 makes it multisampled function gpu_rb_storage(render3d_st: mut Render3dState, rb: int, ifmt: int, w: int, h: int, samples: int) -> void { gvk_flush(render3d_st); render3d_st.gpu_rb_samples = samples render3d_st.gvk_storage_samples = samples gvk_tex_storage(render3d_st, rb, false, ifmt, w, h, 1, false) render3d_st.gvk_storage_samples = 1 } function gpu_fb_color_rb(render3d_st: mut Render3dState, slot: int, rb: int) -> void { gvk_pass_end(render3d_st); let co = gpu_fb_at(render3d_st, render3d_st.gpu_fb_cur); if co >= 0 and slot < 2 { render3d_st.gpu_fb[co + slot] = rb } } function gpu_fb_depth_rb(render3d_st: mut Render3dState, rb: int) -> void { gvk_pass_end(render3d_st); let dop = gpu_fb_at(render3d_st, render3d_st.gpu_fb_cur); if dop >= 0 { render3d_st.gpu_fb[dop + 2] = rb; render3d_st.gpu_fb[dop + 3] = 0 } } # colour slots 0 .. n-1 are drawn into (several: an MRT bake) function gpu_fb_draw_buffers(render3d_st: mut Render3dState, n: int) -> void { gvk_fb_colors(render3d_st, render3d_st.gpu_fb_cur, n) } # a depth-only target: no colour is drawn or read function gpu_fb_no_color(render3d_st: mut Render3dState) -> void { gvk_fb_colors(render3d_st, render3d_st.gpu_fb_cur, 0) } function gpu_fb_status(render3d_st: Render3dState) -> int { return GL_FRAMEBUFFER_COMPLETE } @releases(GpuFramebuffer) function gpu_fb_free(render3d_st: mut Render3dState, fb: int) -> void { gvk_fb_forget(render3d_st, fb); let fo = gpu_fb_at(render3d_st, fb); if fo >= 0 { for i in 0 .. GPU_FB_W { render3d_st.gpu_fb[fo + i] = 0 } } } @releases(GpuRenderbuffer) function gpu_rb_free(render3d_st: mut Render3dState, rb: int) -> void { gvk_flush(render3d_st) gvk_tex_give_back(render3d_st, rb) } function gpu_viewport(render3d_st: mut Render3dState, x: int, y: int, w: int, h: int) -> void { gvk_viewport(render3d_st, x, y, w, h) } function gpu_clear_color(render3d_st: mut Render3dState, r: fixed, g: fixed, b: fixed, a: fixed) -> void { gvk_clear_color(render3d_st, float(r), float(g), float(b), float(a)) } function gpu_clear(render3d_st: mut Render3dState, mask: int) -> void { gvk_clear(render3d_st, mask, render3d_st.gpu_fb, gpu_fb_at(render3d_st, render3d_st.gvk_fb_cur)) } # the bound read framebuffer's [0, w) x [0, h) into the bound draw framebuffer's, unscaled function gpu_blit(render3d_st: mut Render3dState, w: int, h: int, mask: int) -> void { gvk_blit(render3d_st, w, h, mask) } # the framebuffer the finished frame is presented from (an offscreen one, headless) function gpu_screen_fb(render3d_st: Render3dState) -> int { return 0 } function gpu_multisample(render3d_st: mut Render3dState, on: bool) -> void { } # the most samples the scene may be drawn with: the device's colour-and-depth limit on Vulkan (0 before # it is open), 4 on OpenGL, which has always asked for up to that function gpu_msaa_max(render3d_st: Render3dState) -> int { return render3d_st.gvk_msaa_max } function gpu_wireframe(render3d_st: mut Render3dState, on: bool) -> void { render3d_st.gvk_wireframe = gpu_b(on) } # the presented frame as RGB8, bottom row first (a photograph) function gpu_read_screen(render3d_st: mut Render3dState, w: int, h: int, out: pointer) -> void { gvk_read_screen(render3d_st, w, h, out) }