From daf2dc5642e1727fdddfe8b33c52b95bdbadd030 Mon Sep 17 00:00:00 2001 From: bplu4t2f Date: Wed, 30 Sep 2026 13:42:39 +0200 Subject: [PATCH] Add SDL3 callback appmodel example. --- .github/workflows/check.yml | 1 + sdl3/callback-appmodel/README.md | 43 +++++ sdl3/callback-appmodel/bill32.png | Bin 0 -> 415 bytes sdl3/callback-appmodel/main.odin | 245 +++++++++++++++++++++++++ sdl3/callback-appmodel/statistics.odin | 70 +++++++ 5 files changed, 359 insertions(+) create mode 100644 sdl3/callback-appmodel/README.md create mode 100644 sdl3/callback-appmodel/bill32.png create mode 100644 sdl3/callback-appmodel/main.odin create mode 100644 sdl3/callback-appmodel/statistics.odin diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index e15fe0d..12ba3c9 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -135,6 +135,7 @@ jobs: odin check sdl3/microui $FLAGS odin check sdl3/gpu $FLAGS + odin check sdl3/callback-appmodel $FLAGS odin check simd/approaches $FLAGS odin check simd/basic-sum $FLAGS diff --git a/sdl3/callback-appmodel/README.md b/sdl3/callback-appmodel/README.md new file mode 100644 index 0000000..cdd4a70 --- /dev/null +++ b/sdl3/callback-appmodel/README.md @@ -0,0 +1,43 @@ +Callback Appmodel example +========================= + +Implements SDL3's callback based application model in Odin. + +The control flow goes like this: + +``` ++ Odin main +| ++---+> sdl3.RunApp (external) + | + +---+> SDL_AppMain + | + +---+> sdl3.EnterAppMainCallbacks(SDL_AppInit, SDLAppIterate, SDL_AppEvent, SDL_AppQuit) (external) + | + +---> SDL_AppInit + | + +---> SDL_AppIterate + +---> SDL_AppEvent + +---> SDL_AppEvent + +---> SDL_AppIterate + +---> SDL_AppIterate + ... + | + +---> SDL_AppQuit + | + +---+ + | + +---+ + | ++---+ +| ++ +``` + +The rationale behind this application model is that newer platforms, especially mobile, diverge +from the classic "program has one main function and when it returns, it's over" application model. +This gives the OS more control over the resources an application uses, which can be important +for power management and multitasking. + +The purpose behind this architecture is to keep the application's main driver code the same +between different platforms. diff --git a/sdl3/callback-appmodel/bill32.png b/sdl3/callback-appmodel/bill32.png new file mode 100644 index 0000000000000000000000000000000000000000..11522bfc54baac17c1ce4a7c3d95d5ec895bfb90 GIT binary patch literal 415 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%C&rs6b?Si}mUKs7M+SzC z{oH>NSwSk3J%W507^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f-TA0 z-33Sk!B6MiAk#PtJR*x381$4un6YB1eHl=Yy~NYkmHic`9G8J^;AQ)hK%vK;E{-7) zhm#W|5)OEDJ^HW8^S{$*OM6AJQ9T)Urp3ChfYw)V!;cJE@??a6IjADzLxx53r<9<8{axUa}U_9fFRl^g3 z17TN`qW${bubjp c.int { + return sdl3.EnterAppMainCallbacks(argc, argv, SDL_AppInit, SDL_AppIterate, SDL_AppEvent, SDL_AppQuit) +} + +App_State :: struct { + runtime_context: runtime.Context, + window: ^sdl3.Window, + renderer: ^sdl3.Renderer, + bill_texture: ^sdl3.Texture, + window_size_valid: bool, + window_size: [2]i32, + iterate_time: SDL_Time, + keyboard: []bool, + position: [2]f32, +} + +DEFAULT_CANVAS_W :: 800 +DEFAULT_CANVAS_H :: 600 + +SDL_AppInit :: proc "cdecl" (appstate: ^rawptr, argc: c.int, argv: [^]cstring) -> sdl3.AppResult { + + context = runtime.default_context() + context.logger = log.create_console_logger() + + state := new(App_State) + assert(state != nil) + state.runtime_context = context + appstate^ = state + + assert(sdl3.Init({ .VIDEO })) + + state.window = sdl3.CreateWindow("SDL3 Callback AppModel example", DEFAULT_CANVAS_W, DEFAULT_CANVAS_H, {.RESIZABLE}) + assert(state.window != nil) + + state.renderer = sdl3.CreateRenderer(state.window, sdl3.SOFTWARE_RENDERER) + assert(state.renderer != nil) + + assert(sdl3.SetRenderVSync(state.renderer, 1)) + + state.bill_texture = load_texture_from_path(state.renderer, "bill32.png") + assert(state.bill_texture != nil) + + return .CONTINUE +} + +SDL_AppIterate :: proc "cdecl" (appstate: rawptr) -> sdl3.AppResult { + + state := cast(^App_State)appstate + assert_contextless(state != nil) + context = state.runtime_context + + statistics_sw := get_time() + + // Process window changes. + + if !state.window_size_valid { + w, h : c.int + assert(sdl3.GetWindowSizeInPixels(state.window, &w, &h)) + state.window_size = { w, h } + // Resize the viewport. + state.window_size_valid = true + statistics_record_time(.window, &statistics_sw) + } + + // Accumulate elapsed game time (RTC). + + now := get_time() + elapsed_duration := get_duration(state.iterate_time, now) + elapsed_ms := duration_to_ms32(elapsed_duration) + state.iterate_time = now + statistics_record_time(.timing, &statistics_sw) + + // Capture keyboard input state. + + num_keys : c.int + keyboard_state := sdl3.GetKeyboardState(&num_keys) + assert(keyboard_state != nil) + state.keyboard = keyboard_state[:num_keys] + statistics_record_time(.input, &statistics_sw) + + // Apply keyboard input + + SPEED_PX_PER_MS :: 0.1 + if state.keyboard[sdl3.Scancode.LEFT] { + state.position.x -= SPEED_PX_PER_MS * elapsed_ms + } + if state.keyboard[sdl3.Scancode.UP] { + state.position.y -= SPEED_PX_PER_MS * elapsed_ms + } + if state.keyboard[sdl3.Scancode.RIGHT] { + state.position.x += SPEED_PX_PER_MS * elapsed_ms + } + if state.keyboard[sdl3.Scancode.DOWN] { + state.position.y += SPEED_PX_PER_MS * elapsed_ms + } + statistics_record_time(.update_position, &statistics_sw) + + // Clear background + + sdl3.SetRenderDrawColor(state.renderer, 0, 0, 0, 0) + sdl3.RenderClear(state.renderer) + statistics_record_time(.render_clear, &statistics_sw) + + // Calculate bill position + // state.position == (0, 0) means that bill is in the center of the window. + + texture_w, texture_h: f32 + sdl3.GetTextureSize(state.bill_texture, &texture_w, &texture_h) + + render_x := cast(f32)state.window_size.x / 2.0 - texture_w / 2 + state.position.x + render_y := cast(f32)state.window_size.y / 2.0 - texture_h / 2 + state.position.y + + // Render bill + + sdl3.RenderTexture(state.renderer, state.bill_texture, nil, &sdl3.FRect{ render_x, render_y, texture_w, texture_h }) + statistics_record_time(.render_texture, &statistics_sw) + + statistics_render(state.renderer) + statistics_record_time(.render_statistics, &statistics_sw) + + sdl3.RenderPresent(state.renderer) + statistics_record_time(.present, &statistics_sw) + + return .CONTINUE +} + +SDL_AppEvent :: proc "cdecl" (appstate: rawptr, event: ^sdl3.Event) -> sdl3.AppResult { + + state := cast(^App_State)appstate + assert_contextless(state != nil) + context = state.runtime_context + + sw := get_time() + + #partial switch event.type { + + case .QUIT: + return .SUCCESS + + case .KEY_DOWN: + if event.key.scancode == .ESCAPE { + // Quit application. + return .SUCCESS + } + + case .WINDOW_RESIZED: + state.window_size_valid = false + } + + statistics_record_time(.event, &sw) + + return .CONTINUE +} + +SDL_AppQuit :: proc "cdecl" (appstate: rawptr, result: sdl3.AppResult) { + + state := cast(^App_State)appstate + assert_contextless(state != nil) + context = state.runtime_context + + sdl3.DestroyTexture(state.bill_texture) + state.bill_texture = nil + sdl3.DestroyRenderer(state.renderer) + state.renderer = nil + sdl3.DestroyWindow(state.window) + state.window = nil + + free(state) +} + +// Timestamp in SDL time (nanosecond precision). +SDL_Time :: distinct u64 +// Duration in nanoseconds. +SDL_Duration :: distinct i64 + +@(require_results) +get_time :: #force_inline proc "contextless" () -> SDL_Time { + return cast(SDL_Time)sdl3.GetTicksNS() +} + +@(require_results) +get_duration :: #force_inline proc "contextless" (start, end : SDL_Time) -> SDL_Duration { + return cast(SDL_Duration)end - cast(SDL_Duration)start +} + +@(require_results) +get_duration_ms32 :: #force_inline proc "contextless" (start, end : SDL_Time) -> f32 { + return cast(f32)(end - start) / cast(f32)sdl3.NS_PER_MS +} + +@(require_results) +duration_from_ms :: #force_inline proc "contextless" (ms : f64) -> SDL_Duration { + return cast(SDL_Duration)(ms * sdl3.NS_PER_MS) +} + +@(require_results) +duration_to_ms :: #force_inline proc "contextless" (duration : SDL_Duration) -> f64 { + return cast(f64)duration / cast(f64)sdl3.NS_PER_MS +} + +@(require_results) +duration_to_ms32 :: #force_inline proc "contextless" (duration : SDL_Duration) -> f32 { + return cast(f32)duration / cast(f32)sdl3.NS_PER_MS +} + +load_texture_from_path :: proc(renderer: ^sdl3.Renderer, path: cstring) -> ^sdl3.Texture { + surface := sdl3.LoadPNG(path) + assert(surface != nil) + texture := sdl3.CreateTextureFromSurface(renderer, surface) + assert(texture != nil) + return texture +} + +statistics_record_time :: proc(id: Statistic, sw: ^SDL_Time) { + now := get_time() + ms := get_duration_ms32(sw^, now) + sw^ = now + statistics_record(id, ms) +} + +statistics_render :: proc(renderer: ^sdl3.Renderer) { + sdl3.SetRenderDrawColor(renderer, 0, 255, 255, 255) + for i in 0 ..< len(statistics) { + name := statistics[i].name + avg := statistics[i].sum_in_queue / cast(f32)queue.len(statistics[i].queue) + max := statistics[i].max + s := fmt.tprintf("%30v: Avg %.2f - Max %.2f", name, avg, max) + sdl3.RenderDebugText(renderer, 0, cast(f32)(i * 10), auto_cast raw_data(s)) + } +} diff --git a/sdl3/callback-appmodel/statistics.odin b/sdl3/callback-appmodel/statistics.odin new file mode 100644 index 0000000..ed555b1 --- /dev/null +++ b/sdl3/callback-appmodel/statistics.odin @@ -0,0 +1,70 @@ +package sdl3_callback_appmodel_example + +import reflect "core:reflect" +import slice "core:slice" +import queue "core:container/queue" +import fmt "core:fmt" + +Statistic :: enum { + window, + timing, + input, + render_statistics, + present, + event, + + update_position, + render_clear, + render_texture, +} + +Statistic_Sample :: struct { + name: string, + queue: queue.Queue(f32), + sum_in_queue: f32, + max: f32, +} + +STATISTICS_QUEUE_SIZE :: 100 + +statistics : [dynamic]Statistic_Sample + +statistics_get_associative_array_length :: proc() -> int { + values := reflect.enum_field_values(Statistic) + max, ok := slice.max(values) + if !ok { + return 0 + } + return cast(int)max + 1 +} + +statistics_record :: proc(id : Statistic, value : f32) { + if statistics == nil { + length := statistics_get_associative_array_length() + statistics = make([dynamic]Statistic_Sample, length) + for i in 0 ..< length { + statistics[i].name = reflect.enum_string(cast(Statistic)i) + queue.init(&statistics[i].queue, STATISTICS_QUEUE_SIZE) + } + } + sample := &statistics[transmute(int)id] + for queue.len(sample.queue) >= STATISTICS_QUEUE_SIZE { + popped := queue.dequeue(&sample.queue) + sample.sum_in_queue -= popped + } + queue.enqueue(&sample.queue, value) + sample.sum_in_queue += value + if value > sample.max { + sample.max = value + } +} + +statistics_print :: proc() { + fmt.println("Statistics:") + for i in 0 ..< len(statistics) { + name := statistics[i].name + avg := statistics[i].sum_in_queue / cast(f32)queue.len(statistics[i].queue) + max := statistics[i].max + fmt.printfln(" %v: Avg %.2f - Max %.2f", name, avg, max) + } +}