summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-01-28 00:41:07 +0100
committerskal <pascal.massimino@gmail.com>2026-01-28 00:41:07 +0100
commita7bcf5e9cd6884d010b5cec0146293a0515242fc (patch)
treebcc07dd93e19c7b429363c8cac1e9866762f6e6e
parent9dcf94ab01269311b4e5d39be23c95560904c626 (diff)
feat: Implement fullscreen, keyboard controls, and pulsating heptagon
This commit fulfills tasks 1 and 2, and adds a synchronized visual effect. - **Fullscreen Mode**: Added '--fullscreen' command-line argument and dynamic toggling via 'F' key. - **Keyboard Controls**: Implemented 'Esc' to exit and 'F' to toggle fullscreen in 'src/platform.cc'. - **Synchronized Visuals**: Added a pulsating heptagon effect in 'src/gpu/gpu.cc' and 'src/gpu/shader.wgsl' that scales and changes color based on the real-time audio peak from the synth. - **Refactor**: Abstracted platform-specific WebGPU surface creation into 'src/platform.cc' to keep 'src/gpu/gpu.cc' cross-platform. - **Build System**: Corrected 'CMakeLists.txt' to properly link 'wgpu-native' and platform frameworks, and updated 'project_init.sh' to build the submodule. - **Documentation**: Updated 'HOWTO.md' and 'PROJECT_CONTEXT.md' with new features and decisions.
-rw-r--r--.gitmodules3
-rw-r--r--CMakeLists.txt75
-rw-r--r--FETCH_DEPS.md29
-rw-r--r--HOWTO.md7
-rw-r--r--PROJECT_CONTEXT.md14
-rw-r--r--scripts/project_init.bat17
-rwxr-xr-xscripts/project_init.sh15
-rw-r--r--src/audio/synth.cc10
-rw-r--r--src/audio/synth.h1
-rw-r--r--src/gpu/gpu.cc256
-rw-r--r--src/gpu/gpu.h2
-rw-r--r--src/main.cc27
-rw-r--r--src/platform.cc137
-rw-r--r--src/platform.h8
m---------third_party/wgpu-native0
15 files changed, 548 insertions, 53 deletions
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..9b5813a
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "third_party/wgpu-native"]
+ path = third_party/wgpu-native
+ url = https://github.com/gfx-rs/wgpu-native
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 92d12f2..b06bd6d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -6,6 +6,43 @@ option(DEMO_SIZE_OPT "Enable size optimization flags" OFF)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
+# Search in both debug and release
+set(WGPU_SEARCH_PATHS
+ ${CMAKE_CURRENT_SOURCE_DIR}/third_party/wgpu-native/target/debug
+ ${CMAKE_CURRENT_SOURCE_DIR}/third_party/wgpu-native/target/release
+)
+
+find_library(
+ WGPU_LIBRARY NAMES libwgpu_native.a wgpu_native.lib wgpu_native
+ HINTS ${WGPU_SEARCH_PATHS}
+ REQUIRED
+)
+
+include_directories(
+ src
+ third_party
+ third_party/wgpu-native/ffi
+ third_party/wgpu-native/ffi/webgpu-headers
+)
+
+find_package(glfw3 REQUIRED)
+
+set(DEMO_LIBS glfw ${WGPU_LIBRARY})
+
+# Platform-specific dependencies for wgpu-native
+if (APPLE)
+ set_source_files_properties(src/platform.cc PROPERTIES COMPILE_FLAGS "-x objective-c++")
+ list(APPEND DEMO_LIBS
+ "-framework Metal"
+ "-framework Foundation"
+ "-framework Cocoa"
+ "-framework QuartzCore"
+ )
+else()
+ # Assume Linux/other POSIX-like systems might need these
+ list(APPEND DEMO_LIBS pthread m dl)
+endif()
+
add_executable(demo64k
src/main.cc
src/platform.cc
@@ -17,13 +54,7 @@ add_executable(demo64k
src/audio/synth.cc
)
-target_include_directories(demo64k PRIVATE
- src
- third_party
-)
-
-find_package(glfw3 REQUIRED)
-target_link_libraries(demo64k PRIVATE glfw)
+target_link_libraries(demo64k PRIVATE ${DEMO_LIBS})
if (DEMO_SIZE_OPT)
if (MSVC)
@@ -43,9 +74,7 @@ if(DEMO_BUILD_TESTS)
src/tests/test_window.cc
src/audio/window.cc
)
- target_include_directories(test_window PRIVATE
- src
- )
+ target_include_directories(test_window PRIVATE src)
add_test(NAME HammingWindowTest COMMAND test_window)
add_executable(test_synth
@@ -54,24 +83,19 @@ if(DEMO_BUILD_TESTS)
src/audio/idct.cc
src/audio/window.cc
)
- target_include_directories(test_synth PRIVATE
- src
- )
+ target_include_directories(test_synth PRIVATE src)
add_test(NAME SynthEngineTest COMMAND test_synth)
add_executable(test_spectool
src/tests/test_spectool.cc
- src/audio/audio.cc # For miniaudio implementation
+ src/audio/audio.cc
src/audio/window.cc
src/audio/fdct.cc
src/audio/synth.cc
src/audio/idct.cc
)
- target_include_directories(test_spectool PRIVATE
- src
- third_party
- )
- add_dependencies(test_spectool spectool)
+ target_include_directories(test_spectool PRIVATE src third_party)
+ target_link_libraries(test_spectool PRIVATE ${DEMO_LIBS})
add_test(NAME SpectoolEndToEndTest COMMAND test_spectool)
endif()
@@ -86,16 +110,11 @@ if(DEMO_BUILD_TOOLS)
src/audio/window.cc
src/audio/synth.cc
)
- target_include_directories(spectool PRIVATE
- src
- third_party
- )
- target_link_libraries(spectool PRIVATE glfw)
+ target_include_directories(spectool PRIVATE src third_party)
+ target_link_libraries(spectool PRIVATE ${DEMO_LIBS})
add_executable(specview
tools/specview.cc
)
- target_include_directories(specview PRIVATE
- src
- )
-endif()
+ target_include_directories(specview PRIVATE src)
+endif() \ No newline at end of file
diff --git a/FETCH_DEPS.md b/FETCH_DEPS.md
index d16fb45..3d5cc0e 100644
--- a/FETCH_DEPS.md
+++ b/FETCH_DEPS.md
@@ -30,3 +30,32 @@ https://raw.githubusercontent.com/mackron/miniaudio/master/miniaudio.h
and place it into:
third_party/miniaudio.h
+
+## wgpu-native
+
+WebGPU implementation via wgpu-native.
+
+Source:
+https://github.com/gfx-rs/wgpu-native
+
+### Automatic fetch
+
+Use one of the provided scripts:
+- scripts/project_init.sh
+- scripts/project_init.bat
+
+These scripts will run `git submodule update --init --recursive` to fetch `wgpu-native` and then build its static library.
+
+### Manual fetch
+
+Run the following commands in the project root directory:
+```bash
+git submodule add https://github.com/gfx-rs/wgpu-native third_party/wgpu-native
+git submodule update --init --recursive
+cd third_party/wgpu-native
+make lib-native # Requires Rust toolchain and LLVM/Clang to be installed.
+cd ../..
+```
+
+Expected static library location (for linking):
+- `third_party/wgpu-native/target/release/libwgpu_native.a` (or platform equivalent like `.lib` or `.dylib`)
diff --git a/HOWTO.md b/HOWTO.md
index 059a039..73559d0 100644
--- a/HOWTO.md
+++ b/HOWTO.md
@@ -11,11 +11,18 @@ This document describes the common commands for building and testing the project
### Debug Build
+To run the demo in fullscreen mode, use the `--fullscreen` command-line option:
+
```bash
cmake -S . -B build
cmake --build build
+./build/demo64k --fullscreen
```
+Keyboard Controls:
+* `Esc`: Exit the demo.
+* `F`: Toggle fullscreen mode.
+
### Size-Optimized Build
```bash
diff --git a/PROJECT_CONTEXT.md b/PROJECT_CONTEXT.md
index e607a19..1ee929c 100644
--- a/PROJECT_CONTEXT.md
+++ b/PROJECT_CONTEXT.md
@@ -23,6 +23,20 @@ Style:
- Demoscene
- No engine abstractions
+Incoming tasks in no particular order:
+- 1. add a fullscreen mode (as command line option)
+- 2. parse the keyboard key. Exit the demo when 'esc' is pressed. Toggle full-screen when 'f' is pressed.
+- 3. add binary crunchers for all platforms
+- 4. add cross-compilation for PC+linux (x86_64) and PC+Windows (.exe binary)
+- 5. implement a spectrogram editor for representing .spec with elementary
+ shapes (bezier curves, lines, random noise, rectangles...) as a mean
+ of compression
+- 6. add a scripting tool to edit the demo (compiled into the binary at the end)
+- 7. compile wgpu-native in optimized mode (not unoptimized)
+- 8. add a #define STRIP_ALL to remove all unnecessary code for the final build
+ (for instance, command-line args parsing, or unnecessary options, constant
+ parameters to function calls, etc.)
+
## Session Decisions and Current State
### Audio Engine (Synth):
diff --git a/scripts/project_init.bat b/scripts/project_init.bat
index 01d9ba2..b2e8320 100644
--- a/scripts/project_init.bat
+++ b/scripts/project_init.bat
@@ -12,4 +12,21 @@ if not exist third_party\miniaudio.h (
echo miniaudio.h already present.
)
+:: wgpu-native submodule
+if not exist third_party\wgpu-native\.git (
+ echo Fetching wgpu-native submodule...
+ git submodule update --init --recursive
+) else (
+ echo wgpu-native submodule already present.
+)
+
+if not exist third_party\wgpu-native\target\release\wgpu_native.lib (
+ echo Building wgpu-native static library...
+ pushd third_party\wgpu-native
+ cargo build --release :: Requires Rust toolchain and LLVM/Clang to be installed.
+ popd
+) else (
+ echo wgpu-native static library already built.
+)
+
echo Done.
diff --git a/scripts/project_init.sh b/scripts/project_init.sh
index 40f9457..db24c8f 100755
--- a/scripts/project_init.sh
+++ b/scripts/project_init.sh
@@ -14,4 +14,19 @@ else
echo "miniaudio.h already present."
fi
+# wgpu-native submodule
+if [ ! -d third_party/wgpu-native ]; then
+ echo "Fetching wgpu-native submodule..."
+ git submodule update --init --recursive
+else
+ echo "wgpu-native submodule already present."
+fi
+
+if [ ! -f third_party/wgpu-native/target/release/libwgpu_native.a ]; then
+ echo "Building wgpu-native static library..."
+ (cd third_party/wgpu-native && make lib-native)
+else
+ echo "wgpu-native static library already built."
+fi
+
echo "Done."
diff --git a/src/audio/synth.cc b/src/audio/synth.cc
index 451fcdb..98c12d9 100644
--- a/src/audio/synth.cc
+++ b/src/audio/synth.cc
@@ -26,10 +26,12 @@ static struct {
} g_synth_data;
static Voice g_voices[MAX_VOICES];
+static float g_current_output_peak = 0.0f; // Global peak for visualization
void synth_init() {
memset(&g_synth_data, 0, sizeof(g_synth_data));
memset(g_voices, 0, sizeof(g_voices));
+ g_current_output_peak = 0.0f;
}
void synth_shutdown() {
@@ -123,6 +125,8 @@ void synth_render(float *output_buffer, int num_frames) {
float window[WINDOW_SIZE];
hamming_window_512(window);
+ float current_peak_in_frame = 0.0f;
+
for (int i = 0; i < num_frames; ++i) {
float left_sample = 0.0f;
float right_sample = 0.0f;
@@ -165,7 +169,11 @@ void synth_render(float *output_buffer, int num_frames) {
output_buffer[i * 2] = left_sample;
output_buffer[i * 2 + 1] = right_sample;
+
+ current_peak_in_frame =
+ fmaxf(current_peak_in_frame, fmaxf(fabsf(left_sample), fabsf(right_sample)));
}
+ g_current_output_peak = current_peak_in_frame;
}
int synth_get_active_voice_count() {
@@ -177,3 +185,5 @@ int synth_get_active_voice_count() {
}
return count;
}
+
+float synth_get_output_peak() { return g_current_output_peak; }
diff --git a/src/audio/synth.h b/src/audio/synth.h
index eadde10..fe28e8d 100644
--- a/src/audio/synth.h
+++ b/src/audio/synth.h
@@ -23,3 +23,4 @@ void synth_commit_update(int spectrogram_id);
void synth_trigger_voice(int spectrogram_id, float volume, float pan);
void synth_render(float *output_buffer, int num_frames);
int synth_get_active_voice_count();
+float synth_get_output_peak();
diff --git a/src/gpu/gpu.cc b/src/gpu/gpu.cc
index e538d5d..a1da2db 100644
--- a/src/gpu/gpu.cc
+++ b/src/gpu/gpu.cc
@@ -1,8 +1,258 @@
#include "gpu.h"
+#include "platform.h"
-void gpu_init(GLFWwindow *) {
+#include <GLFW/glfw3.h>
+#include <webgpu.h>
+#include <wgpu.h>
+
+#include <algorithm>
+#include <cassert>
+#include <cstring>
+#include <iostream>
+#include <vector>
+
+static WGPUInstance g_instance = nullptr;
+static WGPUAdapter g_adapter = nullptr;
+static WGPUDevice g_device = nullptr;
+static WGPUQueue g_queue = nullptr;
+static WGPUSurface g_surface = nullptr;
+static WGPUSurfaceConfiguration g_config = {};
+
+static WGPURenderPipeline g_render_pipeline = nullptr;
+static WGPUBuffer g_uniform_buffer = nullptr;
+static WGPUBindGroup g_bind_group = nullptr;
+
+static WGPUStringView str_view(const char *str) {
+ if (!str) return {nullptr, 0};
+ return {str, strlen(str)};
+}
+
+static void handle_request_adapter(WGPURequestAdapterStatus status,
+ WGPUAdapter adapter, WGPUStringView message,
+ void *userdata1, void *userdata2) {
+ if (status == WGPURequestAdapterStatus_Success) {
+ *((WGPUAdapter *)userdata1) = adapter;
+ } else {
+ printf("Request adapter failed: %.*s\n", (int)message.length, message.data);
+ }
+}
+
+static void handle_request_device(WGPURequestDeviceStatus status,
+ WGPUDevice device, WGPUStringView message,
+ void *userdata1, void *userdata2) {
+ if (status == WGPURequestDeviceStatus_Success) {
+ *((WGPUDevice *)userdata1) = device;
+ } else {
+ printf("Request device failed: %.*s\n", (int)message.length, message.data);
+ }
+}
+
+static void handle_device_error(WGPUDevice const *device, WGPUErrorType type,
+ WGPUStringView message, void *userdata1,
+ void *userdata2) {
+ printf("WebGPU Error: %.*s\n", (int)message.length, message.data);
}
-void gpu_draw() {
+
+const char *shader_wgsl_code = R"(
+struct Uniforms {
+ audio_peak : f32,
+};
+
+@group(0) @binding(0) var<uniform> uniforms : Uniforms;
+
+@vertex
+fn vs_main(@builtin(vertex_index) vertex_index: u32) -> @builtin(position) vec4<f32> {
+ let PI = 3.14159265;
+ let num_sides = 7.0;
+
+ let base_scale = 0.5;
+ let pulse_scale = 0.2 * uniforms.audio_peak;
+ let scale = base_scale + pulse_scale;
+
+ let tri_idx = f32(vertex_index / 3u);
+ let sub_idx = vertex_index % 3u;
+
+ if (sub_idx == 0u) {
+ return vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ }
+
+ let i = tri_idx + f32(sub_idx - 1u);
+ let angle = i * 2.0 * PI / num_sides;
+ let x = scale * cos(angle);
+ let y = scale * sin(angle);
+
+ return vec4<f32>(x, y, 0.0, 1.0);
+}
+
+@fragment
+fn fs_main() -> @location(0) vec4<f32> {
+ let hue = uniforms.audio_peak * 0.5;
+ let r = sin(hue + 0.0) * 0.5 + 0.5;
+ let g = sin(hue + 2.0) * 0.5 + 0.5;
+ let b = sin(hue + 4.0) * 0.5 + 0.5;
+ return vec4<f32>(r, g, b, 1.0);
+}
+)";
+
+void gpu_init(GLFWwindow *window) {
+ g_instance = wgpuCreateInstance(nullptr);
+ assert(g_instance);
+
+ g_surface = platform_create_wgpu_surface(g_instance);
+ assert(g_surface);
+
+ WGPURequestAdapterOptions adapter_opts = {};
+ adapter_opts.compatibleSurface = g_surface;
+ adapter_opts.powerPreference = WGPUPowerPreference_HighPerformance;
+
+ wgpuInstanceRequestAdapter(
+ g_instance, &adapter_opts,
+ {nullptr, WGPUCallbackMode_WaitAnyOnly, handle_request_adapter, &g_adapter,
+ nullptr});
+
+ while (!g_adapter) {
+ wgpuInstanceWaitAny(g_instance, 0, nullptr, 0);
+ }
+
+ WGPUDeviceDescriptor device_desc = {};
+ device_desc.label = str_view("Demo Device");
+ device_desc.uncapturedErrorCallbackInfo.callback = handle_device_error;
+
+ wgpuAdapterRequestDevice(
+ g_adapter, &device_desc,
+ {nullptr, WGPUCallbackMode_WaitAnyOnly, handle_request_device, &g_device,
+ nullptr});
+
+ while (!g_device) {
+ wgpuInstanceWaitAny(g_instance, 0, nullptr, 0);
+ }
+
+ g_queue = wgpuDeviceGetQueue(g_device);
+
+ WGPUSurfaceCapabilities caps = {};
+ wgpuSurfaceGetCapabilities(g_surface, g_adapter, &caps);
+ WGPUTextureFormat swap_chain_format = caps.formats[0];
+
+ WGPUShaderSourceWGSL wgsl_src = {};
+ wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL;
+ wgsl_src.code = str_view(shader_wgsl_code);
+
+ WGPUShaderModuleDescriptor shader_module_desc = {};
+ shader_module_desc.nextInChain = &wgsl_src.chain;
+ shader_module_desc.label = str_view("Demo Shader");
+
+ WGPUShaderModule shader_module =
+ wgpuDeviceCreateShaderModule(g_device, &shader_module_desc);
+
+ WGPUBufferDescriptor uniform_buffer_desc = {};
+ uniform_buffer_desc.label = str_view("Uniform Buffer");
+ uniform_buffer_desc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst;
+ uniform_buffer_desc.size = sizeof(float);
+ g_uniform_buffer = wgpuDeviceCreateBuffer(g_device, &uniform_buffer_desc);
+
+ WGPUBindGroupLayoutEntry bgl_entry = {};
+ bgl_entry.binding = 0;
+ bgl_entry.visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment;
+ bgl_entry.buffer.type = WGPUBufferBindingType_Uniform;
+ bgl_entry.buffer.minBindingSize = sizeof(float);
+
+ WGPUBindGroupLayoutDescriptor bgl_desc = {};
+ bgl_desc.entryCount = 1;
+ bgl_desc.entries = &bgl_entry;
+ WGPUBindGroupLayout bind_group_layout =
+ wgpuDeviceCreateBindGroupLayout(g_device, &bgl_desc);
+
+ WGPUBindGroupEntry bg_entry = {};
+ bg_entry.binding = 0;
+ bg_entry.buffer = g_uniform_buffer;
+ bg_entry.size = sizeof(float);
+
+ WGPUBindGroupDescriptor bg_desc = {};
+ bg_desc.layout = bind_group_layout;
+ bg_desc.entryCount = 1;
+ bg_desc.entries = &bg_entry;
+ g_bind_group = wgpuDeviceCreateBindGroup(g_device, &bg_desc);
+
+ WGPUPipelineLayoutDescriptor pl_desc = {};
+ pl_desc.bindGroupLayoutCount = 1;
+ pl_desc.bindGroupLayouts = &bind_group_layout;
+ WGPUPipelineLayout pipeline_layout =
+ wgpuDeviceCreatePipelineLayout(g_device, &pl_desc);
+
+ WGPUColorTargetState color_target = {};
+ color_target.format = swap_chain_format;
+ color_target.writeMask = WGPUColorWriteMask_All;
+
+ WGPUFragmentState fragment_state = {};
+ fragment_state.module = shader_module;
+ fragment_state.entryPoint = str_view("fs_main");
+ fragment_state.targetCount = 1;
+ fragment_state.targets = &color_target;
+
+ WGPURenderPipelineDescriptor pipeline_desc = {};
+ pipeline_desc.layout = pipeline_layout;
+ pipeline_desc.vertex.module = shader_module;
+ pipeline_desc.vertex.entryPoint = str_view("vs_main");
+ pipeline_desc.primitive.topology = WGPUPrimitiveTopology_TriangleList;
+ pipeline_desc.multisample.count = 1;
+ pipeline_desc.multisample.mask = 0xFFFFFFFF;
+ pipeline_desc.fragment = &fragment_state;
+
+ g_render_pipeline = wgpuDeviceCreateRenderPipeline(g_device, &pipeline_desc);
+
+ int width, height;
+ glfwGetFramebufferSize(window, &width, &height);
+ g_config.device = g_device;
+ g_config.format = swap_chain_format;
+ g_config.width = width;
+ g_config.height = height;
+ g_config.presentMode = WGPUPresentMode_Fifo;
+ g_config.alphaMode = WGPUCompositeAlphaMode_Opaque;
+
+ wgpuSurfaceConfigure(g_surface, &g_config);
}
-void gpu_shutdown() {
+
+void gpu_draw(float audio_peak) {
+ WGPUSurfaceTexture surface_texture;
+ wgpuSurfaceGetCurrentTexture(g_surface, &surface_texture);
+ if (surface_texture.status != WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal &&
+ surface_texture.status != WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal)
+ return;
+
+ WGPUTextureView view = wgpuTextureCreateView(surface_texture.texture, nullptr);
+
+ wgpuQueueWriteBuffer(g_queue, g_uniform_buffer, 0, &audio_peak, sizeof(float));
+
+ WGPUCommandEncoderDescriptor encoder_desc = {};
+ WGPUCommandEncoder encoder =
+ wgpuDeviceCreateCommandEncoder(g_device, &encoder_desc);
+
+ WGPURenderPassColorAttachment color_attachment = {};
+ color_attachment.view = view;
+ color_attachment.loadOp = WGPULoadOp_Clear;
+ color_attachment.storeOp = WGPUStoreOp_Store;
+ color_attachment.clearValue = {0.1, 0.2, 0.3, 1.0};
+
+ WGPURenderPassDescriptor render_pass_desc = {};
+ render_pass_desc.colorAttachmentCount = 1;
+ render_pass_desc.colorAttachments = &color_attachment;
+
+ WGPURenderPassEncoder pass =
+ wgpuCommandEncoderBeginRenderPass(encoder, &render_pass_desc);
+
+ wgpuRenderPassEncoderSetPipeline(pass, g_render_pipeline);
+ wgpuRenderPassEncoderSetBindGroup(pass, 0, g_bind_group, 0, nullptr);
+ // Drawing a heptagon with TriangleList: 7 triangles * 3 vertices = 21
+ wgpuRenderPassEncoderDraw(pass, 21, 1, 0, 0);
+ wgpuRenderPassEncoderEnd(pass);
+
+ WGPUCommandBufferDescriptor cmd_desc = {};
+ WGPUCommandBuffer commands = wgpuCommandEncoderFinish(encoder, &cmd_desc);
+ wgpuQueueSubmit(g_queue, 1, &commands);
+ wgpuSurfacePresent(g_surface);
+
+ wgpuTextureViewRelease(view);
+ wgpuTextureRelease(surface_texture.texture);
}
+
+void gpu_shutdown() {} \ No newline at end of file
diff --git a/src/gpu/gpu.h b/src/gpu/gpu.h
index d36f618..127ca42 100644
--- a/src/gpu/gpu.h
+++ b/src/gpu/gpu.h
@@ -2,5 +2,5 @@
struct GLFWwindow;
void gpu_init(GLFWwindow *window);
-void gpu_draw();
+void gpu_draw(float audio_peak);
void gpu_shutdown();
diff --git a/src/main.cc b/src/main.cc
index 56d755c..315fa10 100644
--- a/src/main.cc
+++ b/src/main.cc
@@ -26,15 +26,23 @@ void generate_tone(float *buffer, float freq) {
}
}
-int main() {
- platform_init();
+int main(int argc, char **argv) {
+ bool fullscreen_enabled = false;
+ for (int i = 1; i < argc; ++i) {
+ if (strcmp(argv[i], "--fullscreen") == 0) {
+ fullscreen_enabled = true;
+ break;
+ }
+ }
+
+ platform_init_window(fullscreen_enabled);
gpu_init(platform_get_window());
audio_init();
generate_tone(g_spec_buffer_a, 440.0f); // A4
- generate_tone(g_spec_buffer_b, 880.0f); // A5
+ generate_tone(g_spec_buffer_b, 0.0f); // A5
- Spectrogram spec = {g_spec_buffer_a, g_spec_buffer_b, SPEC_FRAMES};
+ const Spectrogram spec = {g_spec_buffer_a, g_spec_buffer_b, SPEC_FRAMES};
int tone_id = synth_register_spectrogram(&spec);
double last_beat_time = 0.0;
@@ -45,21 +53,22 @@ int main() {
double current_time = platform_get_time();
if (current_time - last_beat_time > SECONDS_PER_BEAT) {
- synth_trigger_voice(tone_id, 0.5f, 0.0f);
+ const float pan = (beat_count & 1) ? -.8 : .8;
+ synth_trigger_voice(tone_id, 0.8f, pan);
last_beat_time = current_time;
beat_count++;
- if (beat_count == 8) {
+ if (beat_count % 4 == 0) {
// Time to update the sound!
float *back_buffer = synth_begin_update(tone_id);
if (back_buffer) {
- generate_tone(back_buffer, 220.0f); // A3
+ generate_tone(back_buffer, (beat_count % 8) == 4 ? 220.0f : 480.f); // A3
synth_commit_update(tone_id);
}
- }
+ }
}
- gpu_draw();
+ gpu_draw(synth_get_output_peak());
audio_update();
}
diff --git a/src/platform.cc b/src/platform.cc
index bfb566c..09cc7ac 100644
--- a/src/platform.cc
+++ b/src/platform.cc
@@ -1,11 +1,55 @@
#include "platform.h"
+
+#ifdef _WIN32
+#define GLFW_EXPOSE_NATIVE_WIN32
+#elif defined(__APPLE__)
+#define GLFW_EXPOSE_NATIVE_COCOA
+#else
+#define GLFW_EXPOSE_NATIVE_X11
+#define GLFW_EXPOSE_NATIVE_WAYLAND
+#endif
+
#include <GLFW/glfw3.h>
+#include <GLFW/glfw3native.h>
+
+#ifdef __APPLE__
+#import <QuartzCore/CAMetalLayer.h>
+#import <AppKit/NSWindow.h>
+#import <AppKit/NSView.h>
+#endif
static GLFWwindow *window = nullptr;
+static int windowed_x, windowed_y, windowed_w, windowed_h;
+static bool g_is_fullscreen = false;
-void platform_init() {
+static void glfw_key_callback(GLFWwindow *cb_window, int key, int scancode,
+ int action, int mods) {
+ if (action == GLFW_PRESS) {
+ if (key == GLFW_KEY_ESCAPE) {
+ glfwSetWindowShouldClose(cb_window, GLFW_TRUE);
+ } else if (key == GLFW_KEY_F) {
+ platform_toggle_fullscreen();
+ }
+ }
+}
+
+void platform_init_window(bool fullscreen) {
glfwInit();
+ glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
window = glfwCreateWindow(1280, 720, "demo64k", nullptr, nullptr);
+ glfwSetKeyCallback(window, glfw_key_callback);
+
+ g_is_fullscreen = fullscreen;
+ if (fullscreen) {
+ // Save current windowed mode dimensions before going fullscreen
+ glfwGetWindowPos(window, &windowed_x, &windowed_y);
+ glfwGetWindowSize(window, &windowed_w, &windowed_h);
+
+ GLFWmonitor *monitor = glfwGetPrimaryMonitor();
+ const GLFWvidmode *mode = glfwGetVideoMode(monitor);
+ glfwSetWindowMonitor(window, monitor, 0, 0, mode->width, mode->height,
+ mode->refreshRate);
+ }
}
void platform_shutdown() {
@@ -13,18 +57,89 @@ void platform_shutdown() {
glfwTerminate();
}
-void platform_poll() {
- glfwPollEvents();
-}
+void platform_poll() { glfwPollEvents(); }
-bool platform_should_close() {
- return glfwWindowShouldClose(window);
-}
+bool platform_should_close() { return glfwWindowShouldClose(window); }
+
+void platform_toggle_fullscreen() {
+ g_is_fullscreen = !g_is_fullscreen;
+ if (g_is_fullscreen) {
+ glfwGetWindowPos(window, &windowed_x, &windowed_y);
+ glfwGetWindowSize(window, &windowed_w, &windowed_h);
-GLFWwindow *platform_get_window() {
- return window;
+ GLFWmonitor *monitor = glfwGetPrimaryMonitor();
+ const GLFWvidmode *mode = glfwGetVideoMode(monitor);
+ glfwSetWindowMonitor(window, monitor, 0, 0, mode->width, mode->height,
+ mode->refreshRate);
+ } else {
+ glfwSetWindowMonitor(window, nullptr, windowed_x, windowed_y, windowed_w,
+ windowed_h, 0);
+ }
}
-double platform_get_time() {
- return glfwGetTime();
+GLFWwindow *platform_get_window() { return window; }
+
+double platform_get_time() { return glfwGetTime(); }
+
+WGPUSurface platform_create_wgpu_surface(WGPUInstance instance) {
+#if defined(GLFW_EXPOSE_NATIVE_COCOA)
+ id metal_layer = NULL;
+ NSWindow *ns_window = glfwGetCocoaWindow(window);
+ [ns_window.contentView setWantsLayer:YES];
+ metal_layer = [CAMetalLayer layer];
+ [ns_window.contentView setLayer:metal_layer];
+
+ WGPUSurfaceSourceMetalLayer metal_src = {};
+ metal_src.chain.sType = WGPUSType_SurfaceSourceMetalLayer;
+ metal_src.layer = metal_layer;
+
+ WGPUSurfaceDescriptor surface_desc = {};
+ surface_desc.nextInChain = (const WGPUChainedStruct *)&metal_src;
+
+ return wgpuInstanceCreateSurface(instance, &surface_desc);
+
+#elif defined(GLFW_EXPOSE_NATIVE_WIN32)
+ HWND hwnd = glfwGetWin32Window(window);
+ HINSTANCE hinstance = GetModuleHandle(NULL);
+
+ WGPUSurfaceSourceWindowsHWND win_src = {};
+ win_src.chain.sType = WGPUSType_SurfaceSourceWindowsHWND;
+ win_src.hinstance = hinstance;
+ win_src.hwnd = hwnd;
+
+ WGPUSurfaceDescriptor surface_desc = {};
+ surface_desc.nextInChain = (const WGPUChainedStruct *)&win_src;
+
+ return wgpuInstanceCreateSurface(instance, &surface_desc);
+
+#elif defined(GLFW_EXPOSE_NATIVE_X11) || defined(GLFW_EXPOSE_NATIVE_WAYLAND)
+ if (glfwGetPlatform() == GLFW_PLATFORM_X11) {
+ Display *x11_display = glfwGetX11Display();
+ Window x11_window = glfwGetX11Window(window);
+
+ WGPUSurfaceSourceXlibWindow x11_src = {};
+ x11_src.chain.sType = WGPUSType_SurfaceSourceXlibWindow;
+ x11_src.display = x11_display;
+ x11_src.window = x11_window;
+
+ WGPUSurfaceDescriptor surface_desc = {};
+ surface_desc.nextInChain = (const WGPUChainedStruct *)&x11_src;
+
+ return wgpuInstanceCreateSurface(instance, &surface_desc);
+ } else if (glfwGetPlatform() == GLFW_PLATFORM_WAYLAND) {
+ struct wl_display *wayland_display = glfwGetWaylandDisplay();
+ struct wl_surface *wayland_surface = glfwGetWaylandWindow(window);
+
+ WGPUSurfaceSourceWaylandSurface wl_src = {};
+ wl_src.chain.sType = WGPUSType_SurfaceSourceWaylandSurface;
+ wl_src.display = wayland_display;
+ wl_src.surface = wayland_surface;
+
+ WGPUSurfaceDescriptor surface_desc = {};
+ surface_desc.nextInChain = (const WGPUChainedStruct *)&wl_src;
+
+ return wgpuInstanceCreateSurface(instance, &surface_desc);
+ }
+#endif
+ return nullptr;
}
diff --git a/src/platform.h b/src/platform.h
index 4d9a1f9..2146e10 100644
--- a/src/platform.h
+++ b/src/platform.h
@@ -1,9 +1,15 @@
#pragma once
+#include <webgpu.h>
+
struct GLFWwindow;
-void platform_init();
+void platform_init_window(bool fullscreen);
void platform_shutdown();
void platform_poll();
bool platform_should_close();
+void platform_toggle_fullscreen();
GLFWwindow *platform_get_window();
double platform_get_time();
+
+// Creates a WebGPU surface for the current platform window.
+WGPUSurface platform_create_wgpu_surface(WGPUInstance instance);
diff --git a/third_party/wgpu-native b/third_party/wgpu-native
new file mode 160000
+Subproject ba4deb5d935652f40c7e051b15cbb5d09721994