diff --git a/.gitignore b/.gitignore index f98d936..6b9b7c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .vscode/ build/ -ols.json \ No newline at end of file +ols.json +log.txt \ No newline at end of file diff --git a/README.md b/README.md index 96e7ac0..e1dd288 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/app.odin b/app.odin index e0714fa..c3e0770 100644 --- a/app.odin +++ b/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 diff --git a/releaser/releaser.odin b/releaser/releaser.odin index 91658ee..37c9610 100644 --- a/releaser/releaser.odin +++ b/releaser/releaser.odin @@ -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) diff --git a/reloader/reloader.odin b/reloader/reloader.odin index 6156148..308a651 100644 --- a/reloader/reloader.odin +++ b/reloader/reloader.odin @@ -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) } \ No newline at end of file diff --git a/scripts/build_release.bat b/scripts/build_release.bat index a3af95a..7a3c2c7 100644 --- a/scripts/build_release.bat +++ b/scripts/build_release.bat @@ -1,3 +1,3 @@ @echo off -odin build releaser -out:build/app.exe \ No newline at end of file +odin build releaser -out:build/app.exe -subsystem:windows \ No newline at end of file diff --git a/window/window.odin b/window/window.odin new file mode 100644 index 0000000..bb4909f --- /dev/null +++ b/window/window.odin @@ -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) +} \ No newline at end of file