Cross-platform windowing and graphics (except for web)
This commit is contained in:
parent
e85a2facef
commit
6de280f12d
|
@ -1,3 +1,4 @@
|
|||
.vscode/
|
||||
build/
|
||||
ols.json
|
||||
ols.json
|
||||
log.txt
|
|
@ -3,7 +3,7 @@ Powervessel is a batteries-included framework made for real-time applications,
|
|||
particularly games. It provides:
|
||||
|
||||
- [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
|
||||
- [ ] A Uniform Keyboard + Gamepad Input System
|
||||
- [ ] A Custom Audio System via [MiniAudio](https://miniaud.io)
|
||||
|
|
13
app.odin
13
app.odin
|
@ -1,5 +1,7 @@
|
|||
package app
|
||||
|
||||
import "window"
|
||||
|
||||
// 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.
|
||||
|
||||
|
@ -10,9 +12,18 @@ App_Memory :: struct {
|
|||
g_mem: ^App_Memory
|
||||
|
||||
// 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)
|
||||
app_init :: proc() {
|
||||
app_init :: proc() -> window.Config {
|
||||
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
|
||||
|
|
|
@ -5,6 +5,7 @@ import "core:os"
|
|||
import "core:mem"
|
||||
|
||||
import app ".."
|
||||
import "../window"
|
||||
|
||||
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()
|
||||
context.logger = logger
|
||||
|
||||
// TODO Create Windoww
|
||||
app.app_init()
|
||||
win_config := app.app_init()
|
||||
|
||||
win_ok := window.init(win_config)
|
||||
if !win_ok {
|
||||
log.fatalf("Failed to init Window")
|
||||
return
|
||||
}
|
||||
|
||||
window_open := true
|
||||
for window_open {
|
||||
window_open = app.app_update()
|
||||
window_open = app.app_update() && window.update()
|
||||
|
||||
when USE_TRACKING_ALLOCATOR {
|
||||
for b in tracking_allocator.bad_free_array {
|
||||
|
@ -51,7 +57,7 @@ main :: proc() {
|
|||
|
||||
free_all(context.temp_allocator)
|
||||
app.app_shutdown()
|
||||
// TODO Shutdown Window
|
||||
window.shutdown()
|
||||
|
||||
if logh_err == os.ERROR_NONE {
|
||||
log.destroy_file_logger(logger)
|
||||
|
|
|
@ -10,6 +10,7 @@ import "core:fmt"
|
|||
import "core:log"
|
||||
import "core:c/libc"
|
||||
import "core:mem"
|
||||
import "../window"
|
||||
|
||||
when ODIN_OS == .Windows {
|
||||
DLL_EXT :: ".dll"
|
||||
|
@ -23,7 +24,7 @@ when ODIN_OS == .Windows {
|
|||
}
|
||||
|
||||
Api :: struct {
|
||||
init: proc(),
|
||||
init: proc() -> window.Config,
|
||||
update: proc() -> bool,
|
||||
shutdown: proc(),
|
||||
memory: proc() -> (rawptr, int),
|
||||
|
@ -106,11 +107,16 @@ main :: proc() {
|
|||
|
||||
api_version += 1
|
||||
|
||||
// TODO Create Window
|
||||
api.init()
|
||||
win_config := api.init()
|
||||
|
||||
win_ok := window.init(win_config)
|
||||
if !win_ok {
|
||||
log.fatalf("Failed to init Window")
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
if api.update() == false {
|
||||
if api.update() == false || window.update() == false {
|
||||
break
|
||||
}
|
||||
|
||||
|
@ -132,8 +138,15 @@ main :: proc() {
|
|||
api.shutdown()
|
||||
reset_tracking_allocator(&tracking_allocator)
|
||||
api_unload(api)
|
||||
window.shutdown()
|
||||
|
||||
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 {
|
||||
api_unload(api)
|
||||
api = new_api
|
||||
|
@ -162,7 +175,7 @@ main :: proc() {
|
|||
libc.getchar()
|
||||
}
|
||||
|
||||
// TODO: Shutdown Window
|
||||
window.shutdown()
|
||||
api_unload(api)
|
||||
mem.tracking_allocator_destroy(&tracking_allocator)
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
@echo off
|
||||
|
||||
odin build releaser -out:build/app.exe
|
||||
odin build releaser -out:build/app.exe -subsystem:windows
|
|
@ -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)
|
||||
}
|
Loading…
Reference in New Issue