Cross-platform windowing and graphics (except for web)

This commit is contained in:
Abdulmujeeb Raji 2024-08-16 21:40:42 +01:00
parent e85a2facef
commit 6de280f12d
7 changed files with 380 additions and 14 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.vscode/ .vscode/
build/ build/
ols.json ols.json
log.txt

View File

@ -3,7 +3,7 @@ Powervessel is a batteries-included framework made for real-time applications,
particularly games. It provides: particularly games. It provides:
- [x] Seamless hot-reloading of user code - [x] Seamless hot-reloading of user code
- [ ] Cross-platform windowing and graphics via [GLFW](https://glfw.org) - [x] Cross-platform windowing and graphics via [GLFW](https://glfw.org)
- [ ] An Extensible WebGPU Renderer that supports both 2D and 3D - [ ] An Extensible WebGPU Renderer that supports both 2D and 3D
- [ ] A Uniform Keyboard + Gamepad Input System - [ ] A Uniform Keyboard + Gamepad Input System
- [ ] A Custom Audio System via [MiniAudio](https://miniaud.io) - [ ] A Custom Audio System via [MiniAudio](https://miniaud.io)

View File

@ -1,5 +1,7 @@
package app package app
import "window"
// NOTE: Restart means it's like the program's *just* been opened. Typically happens when memory layout changes // NOTE: Restart means it's like the program's *just* been opened. Typically happens when memory layout changes
// Reload means that the code has changed, but state is still the same. More seamless and you're in the same place in your program. // Reload means that the code has changed, but state is still the same. More seamless and you're in the same place in your program.
@ -10,9 +12,18 @@ App_Memory :: struct {
g_mem: ^App_Memory g_mem: ^App_Memory
// Called upon first run OR full restart. Use it to set starting values (i.e. Player position, menu state) // Called upon first run OR full restart. Use it to set starting values (i.e. Player position, menu state)
// Return value is window settings
@(export) @(export)
app_init :: proc() { app_init :: proc() -> window.Config {
g_mem = new(App_Memory) g_mem = new(App_Memory)
return {
title = "Powervessel Template",
width = 1280,
height = 720,
resizable = true,
clear_color = {1.0, 0.0, 0.0, 1.0}
}
} }
// Typical update loop // Typical update loop

View File

@ -5,6 +5,7 @@ import "core:os"
import "core:mem" import "core:mem"
import app ".." import app ".."
import "../window"
USE_TRACKING_ALLOCATOR :: ODIN_DEBUG USE_TRACKING_ALLOCATOR :: ODIN_DEBUG
@ -31,12 +32,17 @@ main :: proc() {
logger := logh_err == os.ERROR_NONE ? log.create_file_logger(logh) : log.create_console_logger() logger := logh_err == os.ERROR_NONE ? log.create_file_logger(logh) : log.create_console_logger()
context.logger = logger context.logger = logger
// TODO Create Windoww win_config := app.app_init()
app.app_init()
win_ok := window.init(win_config)
if !win_ok {
log.fatalf("Failed to init Window")
return
}
window_open := true window_open := true
for window_open { for window_open {
window_open = app.app_update() window_open = app.app_update() && window.update()
when USE_TRACKING_ALLOCATOR { when USE_TRACKING_ALLOCATOR {
for b in tracking_allocator.bad_free_array { for b in tracking_allocator.bad_free_array {
@ -51,7 +57,7 @@ main :: proc() {
free_all(context.temp_allocator) free_all(context.temp_allocator)
app.app_shutdown() app.app_shutdown()
// TODO Shutdown Window window.shutdown()
if logh_err == os.ERROR_NONE { if logh_err == os.ERROR_NONE {
log.destroy_file_logger(logger) log.destroy_file_logger(logger)

View File

@ -10,6 +10,7 @@ import "core:fmt"
import "core:log" import "core:log"
import "core:c/libc" import "core:c/libc"
import "core:mem" import "core:mem"
import "../window"
when ODIN_OS == .Windows { when ODIN_OS == .Windows {
DLL_EXT :: ".dll" DLL_EXT :: ".dll"
@ -23,7 +24,7 @@ when ODIN_OS == .Windows {
} }
Api :: struct { Api :: struct {
init: proc(), init: proc() -> window.Config,
update: proc() -> bool, update: proc() -> bool,
shutdown: proc(), shutdown: proc(),
memory: proc() -> (rawptr, int), memory: proc() -> (rawptr, int),
@ -106,11 +107,16 @@ main :: proc() {
api_version += 1 api_version += 1
// TODO Create Window win_config := api.init()
api.init()
win_ok := window.init(win_config)
if !win_ok {
log.fatalf("Failed to init Window")
return
}
for { for {
if api.update() == false { if api.update() == false || window.update() == false {
break break
} }
@ -132,8 +138,15 @@ main :: proc() {
api.shutdown() api.shutdown()
reset_tracking_allocator(&tracking_allocator) reset_tracking_allocator(&tracking_allocator)
api_unload(api) api_unload(api)
window.shutdown()
api = new_api api = new_api
api.init() win_config = api.init()
win_ok = window.init(win_config)
if !win_ok {
log.fatalf("Failed to init Window")
return
}
} else { } else {
api_unload(api) api_unload(api)
api = new_api api = new_api
@ -162,7 +175,7 @@ main :: proc() {
libc.getchar() libc.getchar()
} }
// TODO: Shutdown Window window.shutdown()
api_unload(api) api_unload(api)
mem.tracking_allocator_destroy(&tracking_allocator) mem.tracking_allocator_destroy(&tracking_allocator)
} }

View File

@ -1,3 +1,3 @@
@echo off @echo off
odin build releaser -out:build/app.exe odin build releaser -out:build/app.exe -subsystem:windows

335
window/window.odin Normal file
View File

@ -0,0 +1,335 @@
package window
import "base:runtime"
import "core:strings"
import "core:log"
import "core:time"
import "vendor:glfw"
import "vendor:wgpu"
import "vendor:wgpu/glfwglue"
Config :: struct {
title: string,
width, height: int,
resizable: bool,
clear_color: wgpu.Color,
}
g_window: glfw.WindowHandle
g_wgpu_surface: wgpu.Surface
g_wgpu_queue: wgpu.Queue
g_wgpu_device: wgpu.Device
g_clear_color: wgpu.Color
// CURRENT LIMITATION: Only one window can be created per application
init :: proc(win_config: Config) -> bool {
using win_config
title_cstr, err := strings.clone_to_cstring(title, context.temp_allocator)
if err != nil {
log.errorf("Failed to translate title string to cstring")
return false
}
if glfw.Init() == false {
log.errorf("Failed to init GLFW")
return false
}
glfw.WindowHint(glfw.CLIENT_API, glfw.NO_API)
if !resizable {
glfw.WindowHint(glfw.RESIZABLE, glfw.FALSE)
}
g_window = glfw.CreateWindow(i32(width), i32(height), title_cstr, nil, nil)
if g_window == nil {
log.errorf("Failed to create GLFW window")
return false
}
inst_desc := wgpu.InstanceDescriptor{
nextInChain = nil
}
when ODIN_ARCH == .wasm32 {
instance := wgpu.CreateInstance(nil)
} else {
instance := wgpu.CreateInstance(&inst_desc)
}
if instance == nil {
log.errorf("Failed to initialize WebGPU instance")
return false
}
log.debugf("WebGPU Instance: {0}", instance)
g_wgpu_surface = glfwglue.GetSurface(instance, g_window)
log.debugf("Requesting WebGPU Adapter...")
adapter_opts: wgpu.RequestAdapterOptions
adapter_opts.nextInChain = nil
adapter_opts.compatibleSurface = g_wgpu_surface
adapter := request_adapter_sync(instance, &adapter_opts)
log.debugf("Got WebGPU Adapter: {0}", adapter)
when ODIN_DEBUG {
inspect_adapter(adapter)
}
log.debugf("Requesting WebGPU device...")
device_desc := wgpu.DeviceDescriptor{
nextInChain = nil,
label = "Powervessel Engine WebGPU Device",
requiredFeatureCount = 0,
requiredLimits = nil,
defaultQueue = {
nextInChain = nil,
label = "Powervessel Engine Default Queue"
},
deviceLostCallback = proc "cdecl" (reason: wgpu.DeviceLostReason, msg: cstring, raw_user_data: rawptr) {
context = runtime.default_context()
log.debugf("Device lost: reason {0}", reason)
if msg != "" do log.debugf(" ({0})", msg)
},
}
g_wgpu_device = request_device_sync(adapter, &device_desc)
log.debugf("Got WebGPU device: {0}", g_wgpu_device)
on_device_error :: proc "cdecl" (type: wgpu.ErrorType, msg: cstring, raw_user_data: rawptr) {
context = runtime.default_context()
log.debugf("Uncaptured device error: type {0}", type)
if msg != "" do log.debugf(" ({0})", msg)
}
wgpu.DeviceSetUncapturedErrorCallback(g_wgpu_device, on_device_error, nil)
when ODIN_DEBUG {
inspect_device(g_wgpu_device)
}
g_wgpu_queue = wgpu.DeviceGetQueue(g_wgpu_device)
wgpu.QueueOnSubmittedWorkDone(g_wgpu_queue,
proc "cdecl" (status: wgpu.QueueWorkDoneStatus, raw_user_data: rawptr) {
context = runtime.default_context()
log.debugf("Queued work finished with status: {0}", status)
})
surface_format := wgpu.SurfaceGetPreferredFormat(g_wgpu_surface, adapter)
surface_config := wgpu.SurfaceConfiguration{
nextInChain = nil,
width = u32(width),
height = u32(height),
format = surface_format,
viewFormatCount = 0,
viewFormats = nil,
usage = {.RenderAttachment},
device = g_wgpu_device,
presentMode = .Fifo,
alphaMode = .Auto,
}
wgpu.SurfaceConfigure(g_wgpu_surface, &surface_config)
wgpu.AdapterRelease(adapter)
g_clear_color = clear_color
return true
}
update :: proc() -> bool {
if glfw.WindowShouldClose(g_window) do return false
glfw.PollEvents()
rtv := get_render_target_view()
if rtv == nil {
log.fatalf("Failed to get render target view")
return false
}
encoder_desc := wgpu.CommandEncoderDescriptor{
nextInChain = nil,
label = "Powervessel Engine Command Encoder"
}
encoder := wgpu.DeviceCreateCommandEncoder(g_wgpu_device, &encoder_desc)
color_attachment := wgpu.RenderPassColorAttachment{
view = rtv,
resolveTarget = nil,
loadOp = .Clear,
storeOp = .Store,
clearValue = g_clear_color
}
render_pass_desc := wgpu.RenderPassDescriptor{
nextInChain = nil,
colorAttachmentCount = 1,
colorAttachments = &color_attachment,
depthStencilAttachment = nil,
timestampWrites = nil,
}
render_pass := wgpu.CommandEncoderBeginRenderPass(encoder, &render_pass_desc)
wgpu.RenderPassEncoderEnd(render_pass)
wgpu.RenderPassEncoderRelease(render_pass)
buffer_desc := wgpu.CommandBufferDescriptor{
nextInChain = nil,
label = "Powervessel Engine Command Buffer"
}
buffer := wgpu.CommandEncoderFinish(encoder, &buffer_desc)
wgpu.CommandEncoderRelease(encoder)
wgpu.QueueSubmit(g_wgpu_queue, {buffer})
wgpu.CommandBufferRelease(buffer)
wgpu.TextureViewRelease(rtv)
when ODIN_ARCH != .wasm32 {
wgpu.SurfacePresent(g_wgpu_surface)
}
return true
}
shutdown :: proc() {
glfw.DestroyWindow(g_window)
glfw.Terminate()
}
@(private)
request_adapter_sync :: proc(instance: wgpu.Instance, opts: ^wgpu.RequestAdapterOptions) -> wgpu.Adapter {
User_Data :: struct {
adapter: wgpu.Adapter,
request_ended: bool,
odin_ctx: runtime.Context
}
user_data: User_Data
user_data.odin_ctx = context
on_adapter_request_ended :: proc "cdecl" (status: wgpu.RequestAdapterStatus, adapter: wgpu.Adapter, msg: cstring, raw_user_data: rawptr) {
user_data := cast(^User_Data)(raw_user_data)
context = user_data.odin_ctx
if status == .Success {
user_data.adapter = adapter
} else {
log.errorf("Failed to get WebGPU adapter: {0}", msg)
}
user_data.request_ended = true
}
wgpu.InstanceRequestAdapter(instance, opts, on_adapter_request_ended, rawptr(&user_data))
for !user_data.request_ended {
time.sleep(100 * time.Millisecond)
}
assert(user_data.request_ended)
return user_data.adapter
}
@(private)
request_device_sync :: proc(adapter: wgpu.Adapter, desc: ^wgpu.DeviceDescriptor) -> wgpu.Device {
User_Data :: struct {
device: wgpu.Device,
request_ended: bool,
odin_ctx: runtime.Context
}
user_data: User_Data
user_data.odin_ctx = context
on_device_request_end :: proc "cdecl" (status: wgpu.RequestDeviceStatus, device: wgpu.Device, msg: cstring, raw_user_data: rawptr) {
user_data := cast(^User_Data)(raw_user_data)
context = user_data.odin_ctx
if status == .Success {
user_data.device = device
} else {
log.errorf("Failed to get WebGPU device: {0}", msg)
}
user_data.request_ended = true
}
wgpu.AdapterRequestDevice(adapter, desc, on_device_request_end, rawptr(&user_data))
for !user_data.request_ended {
time.sleep(100 * time.Millisecond)
}
assert(user_data.request_ended)
return user_data.device
}
@(private)
inspect_adapter :: proc(adapter: wgpu.Adapter) {
when ODIN_ARCH != .wasm32 {
limits, limits_ok := wgpu.AdapterGetLimits(adapter)
if limits_ok {
log.debugf("Adapter limits:")
log.debugf(" - maxTextureDimension1D: {0}", limits.limits.maxTextureDimension1D)
log.debugf(" - maxTextureDimension2D: {0}", limits.limits.maxTextureDimension2D)
log.debugf(" - maxTextureDimension3D: {0}", limits.limits.maxTextureDimension3D)
log.debugf(" - maxTextureArrayLayers: {0}", limits.limits.maxTextureArrayLayers)
}
}
features := wgpu.AdapterEnumerateFeatures(adapter, context.temp_allocator)
log.debugf("WebGPU Adapter Features:")
for f in features {
log.debugf(" - {0}", f)
}
properties := wgpu.AdapterGetProperties(adapter)
log.debugf("WebGPU Adapter Properties:")
log.debugf(" - vendorID: {0}", properties.vendorID)
log.debugf(" - vendorName: {0}", properties.vendorName)
log.debugf(" - architecture: {0}", properties.architecture)
log.debugf(" - deviceID: {0}", properties.deviceID)
log.debugf(" - name: {0}", properties.name)
log.debugf(" - driverDescription: {0}", properties.driverDescription)
log.debugf(" - adapterType: {0}", properties.adapterType)
log.debugf(" - backendType: {0}", properties.backendType)
}
@(private)
inspect_device :: proc(device: wgpu.Device) {
features := wgpu.DeviceEnumerateFeatures(device, context.temp_allocator)
log.debugf("WebGPU Device Features:")
for f in features {
log.debugf(" - {0}", f)
}
limits, limits_ok := wgpu.DeviceGetLimits(device)
if limits_ok {
log.debugf("Device limits:")
log.debugf(" - maxTextureDimension1D: {0}", limits.limits.maxTextureDimension1D)
log.debugf(" - maxTextureDimension2D: {0}", limits.limits.maxTextureDimension2D)
log.debugf(" - maxTextureDimension3D: {0}", limits.limits.maxTextureDimension3D)
log.debugf(" - maxTextureArrayLayers: {0}", limits.limits.maxTextureArrayLayers)
}
}
@(private)
get_render_target_view :: proc() -> wgpu.TextureView {
surface_tex := wgpu.SurfaceGetCurrentTexture(g_wgpu_surface)
if surface_tex.status != .Success {
log.errorf("Failed to get surface texture: {0}", surface_tex.status)
return nil
}
view_desc := wgpu.TextureViewDescriptor{
nextInChain = nil,
label = "Powervessel Engine Surface Texture View",
format = wgpu.TextureGetFormat(surface_tex.texture),
dimension = ._2D,
baseMipLevel = 0,
mipLevelCount = 1,
baseArrayLayer = 0,
arrayLayerCount = 1,
aspect = .All,
}
return wgpu.TextureCreateView(surface_tex.texture, &view_desc)
}