diff --git a/.gitignore b/.gitignore
index f8bdf755..b5ed333c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,4 @@ windows_install_dependencies
setup
examples/sdl_opengl_and_sokol/example_shader.h
examples/ports/template
+examples/music/music_player
diff --git a/examples/assets/fonts/LICENSE b/examples/assets/fonts/LICENSE
new file mode 100644
index 00000000..d6456956
--- /dev/null
+++ b/examples/assets/fonts/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/examples/assets/fonts/RobotoMono-Regular.ttf b/examples/assets/fonts/RobotoMono-Regular.ttf
new file mode 100644
index 00000000..b158a334
Binary files /dev/null and b/examples/assets/fonts/RobotoMono-Regular.ttf differ
diff --git a/examples/assets/images/tvintris.png b/examples/assets/images/tvintris.png
new file mode 100644
index 00000000..31bc7f58
Binary files /dev/null and b/examples/assets/images/tvintris.png differ
diff --git a/examples/assets/images/v-logo.jpg b/examples/assets/images/v-logo.jpg
new file mode 100644
index 00000000..ad6ae8d8
Binary files /dev/null and b/examples/assets/images/v-logo.jpg differ
diff --git a/examples/assets/images/v-logo.lossless.webp b/examples/assets/images/v-logo.lossless.webp
new file mode 100644
index 00000000..75ecdb05
Binary files /dev/null and b/examples/assets/images/v-logo.lossless.webp differ
diff --git a/examples/assets/images/v-logo.png b/examples/assets/images/v-logo.png
new file mode 100644
index 00000000..adc02d0c
Binary files /dev/null and b/examples/assets/images/v-logo.png differ
diff --git a/examples/assets/images/v-logo.svg b/examples/assets/images/v-logo.svg
new file mode 100644
index 00000000..edd5f393
--- /dev/null
+++ b/examples/assets/images/v-logo.svg
@@ -0,0 +1,142 @@
+
+
+
+
diff --git a/examples/assets/images/v-logo.webp b/examples/assets/images/v-logo.webp
new file mode 100644
index 00000000..7d6609ee
Binary files /dev/null and b/examples/assets/images/v-logo.webp differ
diff --git a/examples/assets/images/v-logo_30_30.png b/examples/assets/images/v-logo_30_30.png
new file mode 100644
index 00000000..ae8c476c
Binary files /dev/null and b/examples/assets/images/v-logo_30_30.png differ
diff --git a/examples/assets/sounds/IcyGarden.mid b/examples/assets/sounds/IcyGarden.mid
new file mode 100644
index 00000000..a8a57dec
Binary files /dev/null and b/examples/assets/sounds/IcyGarden.mid differ
diff --git a/examples/assets/sounds/TwintrisThosenine.mod b/examples/assets/sounds/TwintrisThosenine.mod
new file mode 100644
index 00000000..4fbceed7
Binary files /dev/null and b/examples/assets/sounds/TwintrisThosenine.mod differ
diff --git a/examples/assets/sounds/block.wav b/examples/assets/sounds/block.wav
new file mode 100644
index 00000000..e66eb068
Binary files /dev/null and b/examples/assets/sounds/block.wav differ
diff --git a/examples/assets/sounds/single.wav b/examples/assets/sounds/single.wav
new file mode 100644
index 00000000..bc2fd5c0
Binary files /dev/null and b/examples/assets/sounds/single.wav differ
diff --git a/examples/assets/sounds/sound_01.wav b/examples/assets/sounds/sound_01.wav
new file mode 100644
index 00000000..3279a5ec
Binary files /dev/null and b/examples/assets/sounds/sound_01.wav differ
diff --git a/examples/assets/sounds/sound_02.mp3 b/examples/assets/sounds/sound_02.mp3
new file mode 100644
index 00000000..91406b2e
Binary files /dev/null and b/examples/assets/sounds/sound_02.mp3 differ
diff --git a/examples/assets/sounds/sound_03.flac b/examples/assets/sounds/sound_03.flac
new file mode 100644
index 00000000..8d9a990b
Binary files /dev/null and b/examples/assets/sounds/sound_03.flac differ
diff --git a/examples/assets/sounds/sound_04.ogg b/examples/assets/sounds/sound_04.ogg
new file mode 100644
index 00000000..a09fca76
Binary files /dev/null and b/examples/assets/sounds/sound_04.ogg differ
diff --git a/examples/assets/sounds/triple.wav b/examples/assets/sounds/triple.wav
new file mode 100644
index 00000000..d501c421
Binary files /dev/null and b/examples/assets/sounds/triple.wav differ
diff --git a/examples/music/music_player.v b/examples/music/music_player.v
new file mode 100644
index 00000000..ce010848
--- /dev/null
+++ b/examples/music/music_player.v
@@ -0,0 +1,75 @@
+// Copyright(C) 2026 Delyan Angelov. All rights reserved.
+// Use of this source code is governed by an MIT license
+// that can be found in the LICENSE file.
+module main
+
+import os
+import sdl
+import sdl.mixer as mix
+
+fn main() {
+ if os.args.len < 2 {
+ eprintln('Usage: ${os.args[0]} [audio-file ...]')
+ return
+ }
+
+ if !sdl.init(sdl.init_audio) {
+ eprintln('Could not initialize SDL: ${sdl.get_error_v()}')
+ return
+ }
+ defer { sdl.quit() }
+
+ if !mix.init() {
+ eprintln('Could not initialize SDL_mixer: ${sdl.get_error_v()}')
+ return
+ }
+ defer { mix.quit() }
+
+ mixer := mix.create_mixer_device(sdl.audio_device_default_playback, sdl.null)
+ if mixer == sdl.null {
+ eprintln('Could not open audio device: ${sdl.get_error_v()}')
+ return
+ }
+ defer { mix.destroy_mixer(mixer) }
+
+ track := mix.create_track(mixer)
+ if track == sdl.null {
+ eprintln('Could not create track: ${sdl.get_error_v()}')
+ return
+ }
+ defer { mix.destroy_track(track) }
+
+ for i := 1; i < os.args.len; i++ {
+ path := os.args[i]
+ if !os.exists(path) {
+ eprintln('Skipping missing file: ${path}')
+ continue
+ }
+
+ println('Playing ${path}')
+ audio := mix.load_audio(mixer, path.str, true)
+ if audio == sdl.null {
+ eprintln('Failed to load ${path}: ${sdl.get_error_v()}')
+ continue
+ }
+
+ _ := mix.stop_track(track, 0)
+ if !mix.set_track_audio(track, audio) {
+ eprintln('Failed to set track for ${path}: ${sdl.get_error_v()}')
+ mix.destroy_audio(audio)
+ continue
+ }
+
+ if !mix.play_track(track, sdl.PropertiesID(0)) {
+ eprintln('Failed to play ${path}: ${sdl.get_error_v()}')
+ mix.destroy_audio(audio)
+ continue
+ }
+
+ for mix.track_playing(track) {
+ sdl.delay(25)
+ }
+
+ mix.destroy_audio(audio)
+ }
+}
diff --git a/examples/tvintris/.gitignore b/examples/tvintris/.gitignore
new file mode 100644
index 00000000..9dd8a97d
--- /dev/null
+++ b/examples/tvintris/.gitignore
@@ -0,0 +1 @@
+tvintris
diff --git a/examples/tvintris/README.md b/examples/tvintris/README.md
new file mode 100644
index 00000000..4f238778
--- /dev/null
+++ b/examples/tvintris/README.md
@@ -0,0 +1,17 @@
+# tVintris
+
+tvintris.v is a dual-player (local) version based on original source from tetris example by Alex M.
+It is largely inspired by ancient game Twintris.
+-uses vlib sdl module
+
+
+
+# how to run tVintris
+
+`$ v run .`
+
+# Credits
+
+Colors, Music and Sounds inspired/ripped from amiga title Twintris (1990 nostalgia !)
+- Graphician : Svein Berge
+- Musician : Tor Bernhard Gausen (Walkman/Cryptoburners)
diff --git a/examples/tvintris/tvintris.v b/examples/tvintris/tvintris.v
new file mode 100644
index 00000000..69e1bb79
--- /dev/null
+++ b/examples/tvintris/tvintris.v
@@ -0,0 +1,897 @@
+// Copyright (c) 2019-2022 Alexander Medvednikov. All rights reserved.
+// Use of this source code is governed by an MIT license
+// that can be found in the LICENSE file.
+
+// SDL2 port+wrapper, Twintris-like dual-game logic,
+// and more, by Nicolas Sauzede 2019.
+// V Android support added in 2022 by Lars Pontoppidan.
+
+module main
+
+import rand
+import time
+import os
+import math
+import sdl
+import sdl.image as img
+import sdl.mixer as mix
+import sdl.ttf
+
+const title = 'tVintris'
+const font_name = get_asset_path(os.join_path('fonts', 'RobotoMono-Regular.ttf'))
+const music_name = get_asset_path(os.join_path('sounds', 'TwintrisThosenine.mod'))
+const music_fallback_name = get_asset_path(os.join_path('sounds', 'sound_04.ogg'))
+const snd_block_name = get_asset_path(os.join_path('sounds', 'block.wav'))
+const snd_line_name = get_asset_path(os.join_path('sounds', 'single.wav'))
+const snd_double_name = get_asset_path(os.join_path('sounds', 'triple.wav'))
+const v_logo = get_asset_path(os.join_path('images', 'v-logo_30_30.png'))
+const block_size = 20 // pixels
+
+const field_height = 20 // # of blocks
+
+const field_width = 10
+const tetro_size = 4
+const win_width = block_size * field_width * 3
+const win_height = block_size * field_height
+const timer_period = 250 // ms
+
+const text_size = 16
+const audio_buf_size = 1024
+
+const p2_fire = sdl.KeyCode.l
+const p2_up = sdl.KeyCode.up
+const p2_down = sdl.KeyCode.down
+const p2_left = sdl.KeyCode.left
+const p2_right = sdl.KeyCode.right
+
+const p1_fire = sdl.KeyCode.s
+const p1_up = sdl.KeyCode.w
+const p1_down = sdl.KeyCode.x
+const p1_left = sdl.KeyCode.a
+const p1_right = sdl.KeyCode.d
+
+const n_joy_max = 2
+// joystick name => enter your own device name
+const joy_p1_name = 'Generic X-Box pad'
+// following are joystick button number
+const jb_p1_fire = 1
+// following are joystick hat value
+const jh_p1_up = 1
+const jh_p1_down = 4
+const jh_p1_left = 8
+const jh_p1_right = 3
+
+// joystick name => enter your own device name
+const joy_p2_name = 'RedOctane Guitar Hero X-plorer'
+// following are joystick button number
+const jb_p2_fire = 0
+// following are joystick hat value
+const jh_p2_up = 4
+const jh_p2_down = 1
+const jh_p2_left = 8
+const jh_p2_right = 2
+
+// Tetros' 4 possible states are encoded in binaries
+const b_tetros = [
+ // 0000 0
+ // 0000 0
+ // 0110 6
+ // 0110 6
+ [66, 66, 66, 66],
+ // 0000 0
+ // 0000 0
+ // 0010 2
+ // 0111 7
+ [27, 131, 72, 232],
+ // 0000 0
+ // 0000 0
+ // 0011 3
+ // 0110 6
+ [36, 231, 36, 231],
+ // 0000 0
+ // 0000 0
+ // 0110 6
+ // 0011 3
+ [63, 132, 63, 132],
+ // 0000 0
+ // 0011 3
+ // 0001 1
+ // 0001 1
+ [311, 17, 223, 74],
+ // 0000 0
+ // 0011 3
+ // 0010 2
+ // 0010 2
+ [322, 71, 113, 47],
+ // Special case since 15 can't be used
+ // 1111
+ [1111, 9, 1111, 9],
+]
+// Each tetro has its unique color
+const colors = [
+ sdl.Color{0, 0, 0, 0}, // unused ?
+ sdl.Color{0, 0x62, 0xc0, 0}, // quad : darkblue 0062c0
+ sdl.Color{0xca, 0x7d, 0x5f, 0}, // tricorn : lightbrown ca7d5f
+ sdl.Color{0, 0xc1, 0xbf, 0}, // short topright : lightblue 00c1bf
+ sdl.Color{0, 0xc1, 0, 0}, // short topleft : lightgreen 00c100
+ sdl.Color{0xbf, 0xbe, 0, 0}, // long topleft : yellowish bfbe00
+ sdl.Color{0xd1, 0, 0xbf, 0}, // long topright : pink d100bf
+ sdl.Color{0xd1, 0, 0, 0}, // longest : lightred d10000
+ sdl.Color{0, 170, 170, 0}, // unused ?
+]
+// Background color
+const background_color = sdl.Color{0, 0, 0, 0}
+// Foreground color
+const foreground_color = sdl.Color{0, 170, 170, 0}
+// Text color
+const text_color = sdl.Color{0xca, 0x7d, 0x5f, 0}
+
+// TODO: type Tetro [tetro_size]struct{ x, y int }
+struct Block {
+mut:
+ x int
+ y int
+}
+
+enum GameState {
+ paused
+ running
+ gameover
+}
+
+struct AudioContext {
+mut:
+ mixer &mix.Mixer = sdl.null
+ music &mix.Audio = sdl.null
+ music_track &mix.Track = sdl.null
+ volume f32
+ waves [3]&mix.Audio
+}
+
+struct SdlContext {
+pub mut:
+ // VIDEO
+ w int
+ h int
+ window &sdl.Window = sdl.null
+ renderer &sdl.Renderer = sdl.null
+ screen &sdl.Surface = sdl.null
+ texture &sdl.Texture = sdl.null
+ // AUDIO
+ actx AudioContext
+ // JOYSTICKS
+ jnames [2]string
+ jids [2]sdl.JoystickID
+ // V logo
+ v_logo &sdl.Surface = sdl.null
+ tv_logo &sdl.Texture = sdl.null
+}
+
+fn get_asset_path(path string) string {
+ $if android {
+ return path
+ } $else {
+ return os.resource_abs_path(os.join_path('..', 'assets', path))
+ }
+}
+
+struct Game {
+mut:
+ // Score of the current game
+ score int
+ // Count consecutive lines for scoring
+ lines int
+ // State of the current game
+ state GameState
+ // X offset of the game display
+ ofs_x int
+ // keys
+ k_fire sdl.KeyCode
+ k_up sdl.KeyCode
+ k_down sdl.KeyCode
+ k_left sdl.KeyCode
+ k_right sdl.KeyCode
+ // joystick ID
+ joy_id sdl.JoystickID
+ // joystick buttons
+ jb_fire int
+ // joystick hat values
+ jh_up int
+ jh_down int
+ jh_left int
+ jh_right int
+ // Position of the current tetro
+ pos_x int
+ pos_y int
+ // field[y][x] contains the color of the block with (x,y) coordinates
+ // "-1" border is to avoid bounds checking.
+ // -1 -1 -1 -1
+ // -1 0 0 -1
+ // -1 0 0 -1
+ // -1 -1 -1 -1
+ field [][]int
+ // TODO: tetro Tetro
+ tetro []Block
+ // TODO: tetros_cache []Tetro
+ tetros_cache []Block
+ // Index of the current tetro. Refers to its color.
+ tetro_idx int
+ // Index of the next tetro. Refers to its color.
+ tetro_next int
+ // tetro stats : buckets of drawn tetros
+ tetro_stats []int
+ // total number of drawn tetros
+ tetro_total int
+ // Index of the rotation (0-3)
+ rotation_idx int
+ // SDL2 context for drawing
+ sdl SdlContext
+ // TTF context for font drawing
+ font &ttf.Font = sdl.null
+}
+
+fn (mut sdlc SdlContext) set_sdl_context(w int, h int, titl string) {
+ sdl.init(sdl.init_video | sdl.init_audio | sdl.init_joystick)
+ C.atexit(sdl.quit)
+ ttf.init()
+ C.atexit(ttf.quit)
+ sdl.create_window_and_renderer(titl.str, w, h, sdl.WindowFlags(0), &sdlc.window,
+ &sdlc.renderer)
+ sdlc.w = w
+ sdlc.h = h
+ sdlc.screen = sdl.create_surface(w, h, .argb8888)
+ sdlc.texture = sdl.create_texture(sdlc.renderer, .argb8888, .streaming, w, h)
+ sdl.set_texture_blend_mode(sdlc.texture, sdl.blendmode_none)
+
+ if !mix.init() {
+ println("couldn't init mixer")
+ }
+ C.atexit(mix.quit)
+ sdlc.actx.mixer = mix.create_mixer_device(sdl.audio_device_default_playback, sdl.null)
+ if isnil(sdlc.actx.mixer) {
+ println("couldn't open audio")
+ } else {
+ println('opening music ${music_name}')
+ sdlc.actx.music = mix.load_audio(sdlc.actx.mixer, music_name.str, true)
+ if isnil(sdlc.actx.music) {
+ error_msg := unsafe { cstring_to_vstring(sdl.get_error()) }
+ println('failed to load music: ${error_msg}')
+ println('opening fallback music ${music_fallback_name}')
+ sdlc.actx.music = mix.load_audio(sdlc.actx.mixer, music_fallback_name.str, true)
+ }
+ sdlc.actx.waves[0] = mix.load_audio(sdlc.actx.mixer, snd_block_name.str, true)
+ sdlc.actx.waves[1] = mix.load_audio(sdlc.actx.mixer, snd_line_name.str, true)
+ sdlc.actx.waves[2] = mix.load_audio(sdlc.actx.mixer, snd_double_name.str, true)
+ sdlc.actx.volume = 1.0
+ if !isnil(sdlc.actx.music) {
+ sdlc.actx.music_track = mix.create_track(sdlc.actx.mixer)
+ if !isnil(sdlc.actx.music_track)
+ && mix.set_track_audio(sdlc.actx.music_track, sdlc.actx.music)
+ && mix.play_track(sdlc.actx.music_track, sdl.PropertiesID(0)) {
+ mix.set_track_loops(sdlc.actx.music_track, -1)
+ mix.set_track_gain(sdlc.actx.music_track, sdlc.actx.volume)
+ } else {
+ error_msg := unsafe { cstring_to_vstring(sdl.get_error()) }
+ println('failed to start music: ${error_msg}')
+ }
+ }
+ }
+ mut njoy := 0
+ joys := sdl.get_joysticks(&njoy)
+ if !isnil(joys) {
+ for i in 0 .. njoy {
+ joy_id := unsafe { joys[i] }
+ sdl.open_joystick(joy_id)
+ jn := unsafe { tos_clone(&u8(sdl.get_joystick_name_for_id(joy_id))) }
+ println('JOY NAME ${jn}')
+ for j in 0 .. n_joy_max {
+ if sdlc.jnames[j] == jn {
+ println('FOUND JOYSTICK ${j} ${jn} ID=${joy_id}')
+ sdlc.jids[j] = joy_id
+ }
+ }
+ }
+ sdl.free(joys)
+ }
+ println('opening logo ${v_logo}')
+ sdlc.v_logo = img.load(v_logo.str)
+ if !isnil(sdlc.v_logo) {
+ // println('got v_logo=$sdlc.v_logo')
+ sdlc.tv_logo = sdl.create_texture_from_surface(sdlc.renderer, sdlc.v_logo)
+ // println('got tv_logo=$sdlc.tv_logo')
+ }
+ sdl.set_joystick_events_enabled(true)
+}
+
+fn main() {
+ println('tVintris -- tribute to venerable Twintris')
+ mut game := &Game{}
+ game.sdl.jnames[0] = joy_p1_name
+ game.sdl.jnames[1] = joy_p2_name
+ game.sdl.jids[0] = sdl.JoystickID(0)
+ game.sdl.jids[1] = sdl.JoystickID(0)
+ game.sdl.set_sdl_context(win_width, win_height, title)
+ game.font = ttf.open_font(font_name.str, f32(text_size))
+ mut game2 := &Game{}
+ game2.sdl = game.sdl
+ game2.font = game.font
+
+ game.joy_id = game.sdl.jids[0]
+ // println('JOY1 id=${game.joy_id}')
+ game2.joy_id = game.sdl.jids[1]
+ // println('JOY2 id=${game2.joy_id}')
+
+ // delay uses milliseconds so 1000 ms / 30 frames (30fps) roughly = 33.3333 ms/frame
+ time_per_frame := 1000.0 / 30.0
+
+ game.k_fire = p1_fire
+ game.k_up = p1_up
+ game.k_down = p1_down
+ game.k_left = p1_left
+ game.k_right = p1_right
+ game.jb_fire = jb_p1_fire
+ game.jh_up = jh_p1_up
+ game.jh_down = jh_p1_down
+ game.jh_left = jh_p1_left
+ game.jh_right = jh_p1_right
+ game.ofs_x = 0
+ game.init_game()
+ game.state = .running
+ spawn game.run() // Run the game loop in a new thread
+
+ game2.k_fire = p2_fire
+ game2.k_up = p2_up
+ game2.k_down = p2_down
+ game2.k_left = p2_left
+ game2.k_right = p2_right
+ game2.jb_fire = jb_p2_fire
+ game2.jh_up = jh_p2_up
+ game2.jh_down = jh_p2_down
+ game2.jh_left = jh_p2_left
+ game2.jh_right = jh_p2_right
+ game2.ofs_x = win_width * 2 / 3
+ game2.init_game()
+ game2.state = .running
+ spawn game2.run() // Run the game loop in a new thread
+
+ mut g := Game{}
+ mut should_close := false
+ mut total_frames := u32(0)
+
+ for {
+ total_frames++
+ start_ticks := sdl.get_performance_counter()
+
+ g1 := game
+ g2 := game2
+ // here we determine which game contains most recent state
+ if g1.tetro_total > g.tetro_total {
+ g = *g1
+ }
+ if g2.tetro_total > g.tetro_total {
+ g = *g2
+ }
+ g.draw_begin()
+
+ g1.draw_tetro()
+ g1.draw_field()
+
+ g2.draw_tetro()
+ g2.draw_field()
+
+ g.draw_middle()
+
+ g1.draw_score()
+ g2.draw_score()
+
+ g.draw_stats()
+
+ g.draw_v_logo()
+ g.draw_end()
+
+ // game.handle_events() // CRASHES if done in function ???
+ evt := sdl.Event{}
+ for sdl.poll_event(&evt) {
+ match evt.@type {
+ .quit {
+ should_close = true
+ }
+ .key_down {
+ key := unsafe { sdl.KeyCode(evt.key.key) }
+ if key == sdl.KeyCode.escape {
+ should_close = true
+ break
+ }
+ game.handle_key(key)
+ game2.handle_key(key)
+ }
+ .joystick_button_down {
+ jb := evt.jbutton.button
+ joyid := evt.jbutton.which
+ // println('JOY BUTTON $jb $joyid')
+ game.handle_jbutton(jb, joyid)
+ game2.handle_jbutton(jb, joyid)
+ }
+ .joystick_hat_motion {
+ jh := evt.jhat.hat
+ jv := evt.jhat.value
+ joyid := evt.jhat.which
+ // println('JOY HAT $jh $jv $joyid')
+ game.handle_jhat(jh, jv, joyid)
+ game2.handle_jhat(jh, jv, joyid)
+ }
+ else {}
+ }
+ }
+ if should_close {
+ break
+ }
+ end_ticks := sdl.get_performance_counter()
+ elapsed_time := f64(end_ticks - start_ticks) / f64(sdl.get_performance_frequency())
+ // current_fps := 1.0 / elapsed_time
+
+ // should limit system to (1 / time_per_frame) fps
+ sdl.delay(u32(math.floor(time_per_frame - elapsed_time)))
+ }
+ if !isnil(game.font) {
+ ttf.close_font(game.font)
+ }
+ if !isnil(game.sdl.actx.music_track) {
+ mix.stop_track(game.sdl.actx.music_track, i64(0))
+ mix.destroy_track(game.sdl.actx.music_track)
+ }
+ if !isnil(game.sdl.actx.music) {
+ mix.destroy_audio(game.sdl.actx.music)
+ }
+ if !isnil(game.sdl.actx.waves[0]) {
+ mix.destroy_audio(game.sdl.actx.waves[0])
+ }
+ if !isnil(game.sdl.actx.waves[1]) {
+ mix.destroy_audio(game.sdl.actx.waves[1])
+ }
+ if !isnil(game.sdl.actx.waves[2]) {
+ mix.destroy_audio(game.sdl.actx.waves[2])
+ }
+ if !isnil(game.sdl.actx.mixer) {
+ mix.destroy_mixer(game.sdl.actx.mixer)
+ }
+ if !isnil(game.sdl.tv_logo) {
+ sdl.destroy_texture(game.sdl.tv_logo)
+ }
+ if !isnil(game.sdl.v_logo) {
+ sdl.destroy_surface(game.sdl.v_logo)
+ }
+}
+
+enum Action {
+ idle
+ space
+ fire
+}
+
+@[inline]
+fn (game &Game) fill_rect(s &sdl.Surface, r &sdl.Rect, c &sdl.Color) {
+ sdl.fill_surface_rect(s, r, sdl.map_surface_rgba(s, c.r, c.g, c.b, c.a))
+}
+
+fn (g &Game) play_sfx(idx int) {
+ if idx < 0 || idx >= g.sdl.actx.waves.len {
+ return
+ }
+ if !isnil(g.sdl.actx.mixer) && !isnil(g.sdl.actx.waves[idx]) {
+ mix.play_audio(g.sdl.actx.mixer, g.sdl.actx.waves[idx])
+ }
+}
+
+fn (mut game Game) handle_key(key sdl.KeyCode) {
+ // global keys
+ mut action := Action.idle
+ match key {
+ .space { action = .space }
+ game.k_fire { action = .fire }
+ else {}
+ }
+
+ if action == .space {
+ match game.state {
+ .running {
+ if !isnil(game.sdl.actx.music_track) {
+ mix.pause_track(game.sdl.actx.music_track)
+ }
+ game.state = .paused
+ }
+ .paused {
+ if !isnil(game.sdl.actx.music_track) {
+ mix.resume_track(game.sdl.actx.music_track)
+ }
+ game.state = .running
+ }
+ else {}
+ }
+ }
+
+ if action == .fire {
+ match game.state {
+ .gameover {
+ game.init_game()
+ game.state = .running
+ }
+ else {}
+ }
+ }
+ if game.state != .running {
+ return
+ }
+ // keys while game is running
+ match key {
+ game.k_up { game.rotate_tetro() }
+ game.k_left { game.move_right(-1) }
+ game.k_right { game.move_right(1) }
+ game.k_down { game.move_tetro() } // drop faster when the player presses
+ else {}
+ }
+}
+
+fn (mut game Game) handle_jbutton(jb int, joyid sdl.JoystickID) {
+ if joyid != game.joy_id {
+ return
+ }
+ // global buttons
+ mut action := Action.idle
+ match jb {
+ game.jb_fire { action = .fire }
+ else {}
+ }
+
+ if action == .fire {
+ match game.state {
+ .gameover {
+ game.init_game()
+ game.state = .running
+ }
+ else {}
+ }
+ }
+}
+
+fn (mut game Game) handle_jhat(jh int, jv int, joyid sdl.JoystickID) {
+ if joyid != game.joy_id {
+ return
+ }
+ if game.state != .running {
+ return
+ }
+ // println('testing hat values.. joyid=$joyid jh=$jh jv=$jv')
+ // hat values while game is running
+ match jv {
+ game.jh_up { game.rotate_tetro() }
+ game.jh_left { game.move_right(-1) }
+ game.jh_right { game.move_right(1) }
+ game.jh_down { game.move_tetro() } // drop faster when the player presses
+ else {}
+ }
+}
+
+fn (mut g Game) init_game() {
+ g.score = 0
+ g.tetro_total = 0
+ g.tetro_stats = [0, 0, 0, 0, 0, 0, 0]
+ g.parse_tetros()
+ g.generate_tetro()
+ g.field = []
+ // Generate the field, fill it with 0's, add -1's on each edge
+ for _ in 0 .. field_height + 2 {
+ mut row := [0].repeat(field_width + 2)
+ row[0] = -1
+ row[field_width + 1] = -1
+ g.field << row
+ }
+ mut first_row := g.field[0]
+ mut last_row := g.field[field_height + 1]
+ for j in 0 .. field_width + 2 {
+ first_row[j] = -1
+ last_row[j] = -1
+ }
+}
+
+fn (mut g Game) parse_tetros() {
+ for b_tetro_1 in b_tetros {
+ for b_tetro in b_tetro_1 {
+ for t in parse_binary_tetro(b_tetro) {
+ g.tetros_cache << t
+ }
+ }
+ }
+}
+
+fn (mut g Game) run() {
+ for {
+ if g.state == .running {
+ g.move_tetro()
+ n := g.delete_completed_lines()
+ if n > 0 {
+ g.lines += n
+ } else {
+ if g.lines > 0 {
+ if g.lines > 1 {
+ g.play_sfx(2)
+ } else if g.lines == 1 {
+ g.play_sfx(1)
+ }
+ g.score += 10 * g.lines * g.lines
+ g.lines = 0
+ }
+ }
+ }
+ time.sleep(timer_period * time.millisecond) // medium delay between game step
+ }
+}
+
+fn (mut game Game) rotate_tetro() {
+ // Rotate the tetro
+ old_rotation_idx := game.rotation_idx
+ game.rotation_idx++
+ if game.rotation_idx == tetro_size {
+ game.rotation_idx = 0
+ }
+ game.get_tetro()
+ if !game.move_right(0) {
+ game.rotation_idx = old_rotation_idx
+ game.get_tetro()
+ }
+ if game.pos_x < 0 {
+ game.pos_x = 1
+ }
+}
+
+fn (mut g Game) move_tetro() {
+ // Check each block in current tetro
+ for block in g.tetro {
+ y := block.y + g.pos_y + 1
+ x := block.x + g.pos_x
+ // Reached the bottom of the screen or another block?
+ // TODO: if g.field[y][x] != 0
+ // if g.field[y][x] != 0 {
+ row := g.field[y]
+ if row[x] != 0 {
+ // The new tetro has no space to drop => end of the game
+ if g.pos_y < 2 {
+ g.state = .gameover
+ g.tetro_total = 0
+ return
+ }
+ // Drop it and generate a new one
+ g.drop_tetro()
+ g.generate_tetro()
+ g.play_sfx(0)
+ return
+ }
+ }
+ g.pos_y++
+}
+
+fn (mut g Game) move_right(dx int) bool {
+ // Reached left/right edge or another tetro?
+ for i in 0 .. tetro_size {
+ tetro := g.tetro[i]
+ y := tetro.y + g.pos_y
+ x := tetro.x + g.pos_x + dx
+ row := g.field[y]
+ if row[x] != 0 {
+ // Do not move
+ return false
+ }
+ }
+ g.pos_x += dx
+ return true
+}
+
+fn (g &Game) delete_completed_lines() int {
+ mut n := 0
+ for y := field_height; y >= 1; y-- {
+ n += g.delete_completed_line(y)
+ }
+ return n
+}
+
+fn (g &Game) delete_completed_line(y int) int {
+ for x := 1; x <= field_width; x++ {
+ f := g.field[y]
+ if f[x] == 0 {
+ return 0
+ }
+ }
+ // Move everything down by 1 position
+ for yy := y - 1; yy >= 1; yy-- {
+ for x := 1; x <= field_width; x++ {
+ mut a := g.field[yy + 1]
+ b := g.field[yy]
+ a[x] = b[x]
+ }
+ }
+ return 1
+}
+
+// Draw a rand tetro index
+fn (mut g Game) rand_tetro() int {
+ cur := g.tetro_next
+ g.tetro_next = rand.int_in_range(0, b_tetros.len) or { 0 }
+ return cur
+}
+
+// Place a new tetro on top
+fn (mut g Game) generate_tetro() {
+ g.pos_y = 0
+ g.pos_x = field_width / 2 - tetro_size / 2
+ g.tetro_idx = g.rand_tetro()
+ // println('idx=${g.tetro_idx}')
+ g.tetro_stats[g.tetro_idx] += 2 - 1
+ g.tetro_total++
+ g.rotation_idx = 0
+ g.get_tetro()
+}
+
+// Get the right tetro from cache
+fn (mut g Game) get_tetro() {
+ idx := g.tetro_idx * tetro_size * tetro_size + g.rotation_idx * tetro_size
+ g.tetro = g.tetros_cache[idx..idx + tetro_size]
+}
+
+fn (g &Game) drop_tetro() {
+ for i in 0 .. tetro_size {
+ tetro := g.tetro[i]
+ x := tetro.x + g.pos_x
+ y := tetro.y + g.pos_y
+ // Remember the color of each block
+ // TODO: g.field[y][x] = g.tetro_idx + 1
+ mut row := g.field[y]
+ row[x] = g.tetro_idx + 1
+ }
+}
+
+fn (g &Game) draw_tetro() {
+ for i in 0 .. tetro_size {
+ tetro := g.tetro[i]
+ g.draw_block(g.pos_y + tetro.y, g.pos_x + tetro.x, g.tetro_idx + 1)
+ }
+}
+
+fn (g &Game) draw_block(i int, j int, color_idx int) {
+ rect := sdl.Rect{g.ofs_x + (j - 1) * block_size, (i - 1) * block_size, block_size - 1, block_size - 1}
+ col := colors[color_idx]
+ g.fill_rect(g.sdl.screen, &rect, &col)
+}
+
+fn (g &Game) draw_field() {
+ for i := 1; i < field_height + 1; i++ {
+ for j := 1; j < field_width + 1; j++ {
+ f := g.field[i]
+ if f[j] > 0 {
+ g.draw_block(i, j, f[j])
+ }
+ }
+ }
+}
+
+fn (g &Game) draw_v_logo() {
+ if isnil(g.sdl.tv_logo) {
+ return
+ }
+ mut texw := f32(0)
+ mut texh := f32(0)
+ sdl.get_texture_size(g.sdl.tv_logo, &texw, &texh)
+ dstrect := sdl.FRect{(f32(win_width) - texw) / 2, f32(20), texw, texh}
+ sdl.render_texture(g.sdl.renderer, g.sdl.tv_logo, sdl.null, &dstrect)
+}
+
+fn (g &Game) draw_text(x int, y int, text string, tcol sdl.Color) {
+ tcol_ := sdl.Color{tcol.r, tcol.g, tcol.b, tcol.a}
+ tsurf := ttf.render_text_solid(g.font, text, tcol_)
+ ttext := sdl.create_texture_from_surface(g.sdl.renderer, tsurf)
+ mut texw := f32(0)
+ mut texh := f32(0)
+ sdl.get_texture_size(ttext, &texw, &texh)
+ dstrect := sdl.FRect{f32(x), f32(y), texw, texh}
+ sdl.render_texture(g.sdl.renderer, ttext, sdl.null, &dstrect)
+ sdl.destroy_texture(ttext)
+ sdl.destroy_surface(tsurf)
+}
+
+@[inline]
+fn (g &Game) draw_ptext(x int, y int, text string, tcol sdl.Color) {
+ g.draw_text(g.ofs_x + x, y, text, tcol)
+}
+
+@[live]
+fn (g &Game) draw_begin() {
+ // println('about to clear')
+ sdl.render_clear(g.sdl.renderer)
+ mut rect := sdl.Rect{0, 0, g.sdl.w, g.sdl.h}
+ col := sdl.Color{0, 0, 0, 0}
+ // sdl_fill_rect(g.sdl.screen, &rect, background_color)
+ g.fill_rect(g.sdl.screen, &rect, col)
+
+ rect = sdl.Rect{block_size * field_width + 2, 0, 2, g.sdl.h}
+ g.fill_rect(g.sdl.screen, &rect, foreground_color)
+ rect = sdl.Rect{win_width - block_size * field_width - 4, 0, 2, g.sdl.h}
+ g.fill_rect(g.sdl.screen, &rect, foreground_color)
+
+ mut idx := 0
+ for st in g.tetro_stats {
+ mut s := 10
+ if g.tetro_total > 0 {
+ s += 90 * st / g.tetro_total
+ }
+ w := block_size
+ h := s * 4 * w / 100
+ rect = sdl.Rect{(win_width - 7 * (w + 1)) / 2 + idx * (w + 1), win_height * 3 / 4 - h, w, h}
+ g.fill_rect(g.sdl.screen, &rect, colors[idx + 1])
+ idx++
+ }
+}
+
+fn (g &Game) draw_middle() {
+ sdl.update_texture(g.sdl.texture, sdl.null, g.sdl.screen.pixels, g.sdl.screen.pitch)
+ sdl.render_texture(g.sdl.renderer, g.sdl.texture, sdl.null, sdl.null)
+}
+
+fn (g &Game) draw_score() {
+ if !isnil(g.font) {
+ g.draw_ptext(1, 2, 'score: ' + g.score.str() + ' nxt=' + g.tetro_next.str(), text_color)
+ if g.state == .gameover {
+ g.draw_ptext(1, win_height / 2 + 0 * text_size, 'Game Over', text_color)
+ g.draw_ptext(1, win_height / 2 + 2 * text_size, 'FIRE to restart', text_color)
+ } else if g.state == .paused {
+ g.draw_ptext(1, win_height / 2 + 0 * text_size, 'Game Paused', text_color)
+ g.draw_ptext(1, win_height / 2 + 2 * text_size, 'SPACE to resume', text_color)
+ }
+ }
+}
+
+fn (g &Game) draw_stats() {
+ if !isnil(g.font) {
+ g.draw_text(win_width / 3 + 10, win_height * 3 / 4 + 0 * text_size, 'stats: ' +
+ g.tetro_total.str() + ' tetros', text_color)
+ mut stats := ''
+ for st in g.tetro_stats {
+ mut s := 0
+ if g.tetro_total > 0 {
+ s = 100 * st / g.tetro_total
+ }
+ stats += ' '
+ stats += s.str()
+ }
+ g.draw_text(win_width / 3 - 8, win_height * 3 / 4 + 2 * text_size, stats, text_color)
+ }
+}
+
+fn (g &Game) draw_end() {
+ sdl.render_present(g.sdl.renderer)
+}
+
+fn parse_binary_tetro(t_ int) []Block {
+ mut t := t_
+ res := [Block{}].repeat(4)
+ mut cnt := 0
+ horizontal := t == 9 // special case for the horizontal line
+ for i := 0; i <= 3; i++ {
+ // Get ith digit of t
+ p := int(math.pow(10, 3 - i))
+ mut digit := t / p
+ t %= p
+ // Convert the digit to binary
+ for j := 3; j >= 0; j-- {
+ bin := digit % 2
+ digit /= 2
+ if bin == 1 || (horizontal && i == tetro_size - 1) {
+ // TODO: res[cnt].x = j
+ // res[cnt].y = i
+ mut point := &res[cnt]
+ point.x = j
+ point.y = i
+ cnt++
+ }
+ }
+ }
+ return res
+}
diff --git a/image/README.md b/image/README.md
new file mode 100644
index 00000000..94984c3d
--- /dev/null
+++ b/image/README.md
@@ -0,0 +1,4 @@
+# SDL3 Image Library
+Assuming you've installed the dependencies for sdl (already include `SDL3_image` dependency)
+
+See the Tvintris example for usage
diff --git a/image/c.c.v b/image/c.c.v
new file mode 100644
index 00000000..5fd82784
--- /dev/null
+++ b/image/c.c.v
@@ -0,0 +1,24 @@
+module image
+
+$if !windows {
+ // SDL libs are loaded dynamically from Java on Android
+ $if !android || termux {
+ // sdl_no_compile_flags allow users to provide
+ // custom flags (e.g. via CFLAGS/LDFLAGS) for the compiler.
+ // This is especially useful when building/linking against a
+ // custom compiled version of the libs on *nix.
+ $if !sdl_no_compile_flags ? {
+ #flag -lSDL3_image
+ }
+ }
+}
+
+$if x64 {
+ #flag windows -L @VMODROOT/thirdparty/SDL3_image-3.4.0/lib/x64
+} $else {
+ #flag windows -L @VMODROOT/thirdparty/SDL3_image-3.4.0/lib/x86
+}
+#flag windows -I @VMODROOT/thirdparty/SDL3_image-3.4.0/include
+#flag windows -lSDL3_image
+
+#include
diff --git a/image/image.c.v b/image/image.c.v
new file mode 100644
index 00000000..c0aeabd8
--- /dev/null
+++ b/image/image.c.v
@@ -0,0 +1,253 @@
+// Copyright(C) 2026 Lars Pontoppidan. All rights reserved.
+// Use of this source code is governed by an MIT license
+// that can be found in the LICENSE file.
+module image
+
+//
+// SDL_image.h
+//
+import sdl
+
+pub const major_version = C.SDL_IMAGE_MAJOR_VERSION // 3
+
+pub const minor_version = C.SDL_IMAGE_MINOR_VERSION // 4
+
+pub const micro_version = C.SDL_IMAGE_MICRO_VERSION // 0
+pub const patchlevel = micro_version
+
+// compiledversion is the version number macro for the current SDL_image version.
+pub fn compiledversion() int {
+ return C.SDL_VERSIONNUM(major_version, minor_version, micro_version)
+}
+
+// This macro will evaluate to true if compiled with SDL_image at least X.Y.Z.
+fn C.SDL_IMAGE_VERSION_ATLEAST(x int, y int, z int) bool
+
+// version_atleast evaluates to true if compiled with SDL_image at least X.Y.Z.
+pub fn version_atleast(x int, y int, z int) bool {
+ return C.SDL_IMAGE_VERSION_ATLEAST(x, y, z)
+}
+
+fn C.IMG_LoadTyped_IO(src &C.SDL_IOStream, closeio bool, const_type &char) &C.SDL_Surface
+
+// load_typed_io loads an image from an SDL data source.
+// The 'type' may be one of: "BMP", "GIF", "PNG", etc.
+//
+// If the image format supports a transparent pixel, SDL will set the
+// colorkey for the surface. You can enable RLE acceleration on the
+// surface afterwards by calling:
+// SDL_SetColorKey(image, SDL_RLEACCEL, image->format->colorkey);
+pub fn load_typed_io(src &sdl.RWops, closeio bool, const_type &char) &sdl.Surface {
+ return C.IMG_LoadTyped_IO(src, closeio, const_type)
+}
+
+// Convenience functions
+fn C.IMG_Load(file &char) &C.SDL_Surface
+pub fn load(file &char) &sdl.Surface {
+ return C.IMG_Load(file)
+}
+
+fn C.IMG_Load_IO(src &C.SDL_IOStream, closeio bool) &C.SDL_Surface
+pub fn load_io(src &sdl.RWops, closeio bool) &sdl.Surface {
+ return C.IMG_Load_IO(src, closeio)
+}
+
+fn C.IMG_LoadTexture(renderer &C.SDL_Renderer, const_file &char) &C.SDL_Texture
+
+// load_texture loads an image directly into a render texture.
+pub fn load_texture(renderer &sdl.Renderer, const_file &char) &sdl.Texture {
+ return C.IMG_LoadTexture(renderer, const_file)
+}
+
+fn C.IMG_LoadTexture_IO(renderer &C.SDL_Renderer, src &C.SDL_IOStream, closeio bool) &C.SDL_Texture
+pub fn load_texture_io(renderer &sdl.Renderer, src &sdl.RWops, closeio bool) &sdl.Texture {
+ return C.IMG_LoadTexture_IO(renderer, src, closeio)
+}
+
+fn C.IMG_LoadTextureTyped_IO(renderer &C.SDL_Renderer, src &C.SDL_IOStream, closeio bool, const_type &char) &C.SDL_Texture
+pub fn load_texture_typed_io(renderer &sdl.Renderer, src &sdl.RWops, closeio bool, const_type &char) &sdl.Texture {
+ return C.IMG_LoadTextureTyped_IO(renderer, src, closeio, const_type)
+}
+
+// Functions to detect a file type, given a seekable source
+fn C.IMG_isICO(src &C.SDL_IOStream) bool
+pub fn is_ico(src &sdl.RWops) bool {
+ return C.IMG_isICO(src)
+}
+
+fn C.IMG_isCUR(src &C.SDL_IOStream) bool
+pub fn is_cur(src &sdl.RWops) bool {
+ return C.IMG_isCUR(src)
+}
+
+fn C.IMG_isBMP(src &C.SDL_IOStream) bool
+pub fn is_bmp(src &sdl.RWops) bool {
+ return C.IMG_isBMP(src)
+}
+
+fn C.IMG_isGIF(src &C.SDL_IOStream) bool
+pub fn is_gif(src &sdl.RWops) bool {
+ return C.IMG_isGIF(src)
+}
+
+fn C.IMG_isJPG(src &C.SDL_IOStream) bool
+pub fn is_jpg(src &sdl.RWops) bool {
+ return C.IMG_isJPG(src)
+}
+
+fn C.IMG_isLBM(src &C.SDL_IOStream) bool
+pub fn is_lbm(src &sdl.RWops) bool {
+ return C.IMG_isLBM(src)
+}
+
+fn C.IMG_isPCX(src &C.SDL_IOStream) bool
+pub fn is_pcx(src &sdl.RWops) bool {
+ return C.IMG_isPCX(src)
+}
+
+fn C.IMG_isPNG(src &C.SDL_IOStream) bool
+pub fn is_png(src &sdl.RWops) bool {
+ return C.IMG_isPNG(src)
+}
+
+fn C.IMG_isPNM(src &C.SDL_IOStream) bool
+pub fn is_pnm(src &sdl.RWops) bool {
+ return C.IMG_isPNM(src)
+}
+
+fn C.IMG_isSVG(src &C.SDL_IOStream) bool
+pub fn is_svg(src &sdl.RWops) bool {
+ return C.IMG_isSVG(src)
+}
+
+fn C.IMG_isTIF(src &C.SDL_IOStream) bool
+pub fn is_tif(src &sdl.RWops) bool {
+ return C.IMG_isTIF(src)
+}
+
+fn C.IMG_isXCF(src &C.SDL_IOStream) bool
+pub fn is_xcf(src &sdl.RWops) bool {
+ return C.IMG_isXCF(src)
+}
+
+fn C.IMG_isXPM(src &C.SDL_IOStream) bool
+pub fn is_xpm(src &sdl.RWops) bool {
+ return C.IMG_isXPM(src)
+}
+
+fn C.IMG_isXV(src &C.SDL_IOStream) bool
+pub fn is_xv(src &sdl.RWops) bool {
+ return C.IMG_isXV(src)
+}
+
+fn C.IMG_isWEBP(src &C.SDL_IOStream) bool
+pub fn is_webp(src &sdl.RWops) bool {
+ return C.IMG_isWEBP(src)
+}
+
+// Individual loading functions
+fn C.IMG_LoadICO_IO(src &C.SDL_IOStream) &C.SDL_Surface
+pub fn load_ico_io(src &sdl.RWops) &sdl.Surface {
+ return C.IMG_LoadICO_IO(src)
+}
+
+fn C.IMG_LoadCUR_IO(src &C.SDL_IOStream) &C.SDL_Surface
+pub fn load_cur_io(src &sdl.RWops) &sdl.Surface {
+ return C.IMG_LoadCUR_IO(src)
+}
+
+fn C.IMG_LoadBMP_IO(src &C.SDL_IOStream) &C.SDL_Surface
+pub fn load_bmp_io(src &sdl.RWops) &sdl.Surface {
+ return C.IMG_LoadBMP_IO(src)
+}
+
+fn C.IMG_LoadGIF_IO(src &C.SDL_IOStream) &C.SDL_Surface
+pub fn load_gif_io(src &sdl.RWops) &sdl.Surface {
+ return C.IMG_LoadGIF_IO(src)
+}
+
+fn C.IMG_LoadJPG_IO(src &C.SDL_IOStream) &C.SDL_Surface
+pub fn load_jpg_io(src &sdl.RWops) &sdl.Surface {
+ return C.IMG_LoadJPG_IO(src)
+}
+
+fn C.IMG_LoadLBM_IO(src &C.SDL_IOStream) &C.SDL_Surface
+pub fn load_lbm_io(src &sdl.RWops) &sdl.Surface {
+ return C.IMG_LoadLBM_IO(src)
+}
+
+fn C.IMG_LoadPCX_IO(src &C.SDL_IOStream) &C.SDL_Surface
+pub fn load_pcx_io(src &sdl.RWops) &sdl.Surface {
+ return C.IMG_LoadPCX_IO(src)
+}
+
+fn C.IMG_LoadPNG_IO(src &C.SDL_IOStream) &C.SDL_Surface
+pub fn load_png_io(src &sdl.RWops) &sdl.Surface {
+ return C.IMG_LoadPNG_IO(src)
+}
+
+fn C.IMG_LoadPNM_IO(src &C.SDL_IOStream) &C.SDL_Surface
+pub fn load_pnm_io(src &sdl.RWops) &sdl.Surface {
+ return C.IMG_LoadPNM_IO(src)
+}
+
+fn C.IMG_LoadSVG_IO(src &C.SDL_IOStream) &C.SDL_Surface
+pub fn load_svg_io(src &sdl.RWops) &sdl.Surface {
+ return C.IMG_LoadSVG_IO(src)
+}
+
+fn C.IMG_LoadTGA_IO(src &C.SDL_IOStream) &C.SDL_Surface
+pub fn load_tga_io(src &sdl.RWops) &sdl.Surface {
+ return C.IMG_LoadTGA_IO(src)
+}
+
+fn C.IMG_LoadTIF_IO(src &C.SDL_IOStream) &C.SDL_Surface
+pub fn load_tif_io(src &sdl.RWops) &sdl.Surface {
+ return C.IMG_LoadTIF_IO(src)
+}
+
+fn C.IMG_LoadXCF_IO(src &C.SDL_IOStream) &C.SDL_Surface
+pub fn load_xcf_io(src &sdl.RWops) &sdl.Surface {
+ return C.IMG_LoadXCF_IO(src)
+}
+
+fn C.IMG_LoadXPM_IO(src &C.SDL_IOStream) &C.SDL_Surface
+pub fn load_xpm_io(src &sdl.RWops) &sdl.Surface {
+ return C.IMG_LoadXPM_IO(src)
+}
+
+fn C.IMG_LoadXV_IO(src &C.SDL_IOStream) &C.SDL_Surface
+pub fn load_xv_io(src &sdl.RWops) &sdl.Surface {
+ return C.IMG_LoadXV_IO(src)
+}
+
+fn C.IMG_LoadWEBP_IO(src &C.SDL_IOStream) &C.SDL_Surface
+pub fn load_webp_io(src &sdl.RWops) &sdl.Surface {
+ return C.IMG_LoadWEBP_IO(src)
+}
+
+fn C.IMG_ReadXPMFromArray(xpm &&char) &C.SDL_Surface
+pub fn read_xpm_from_array(xpm &&char) &sdl.Surface {
+ return C.IMG_ReadXPMFromArray(xpm)
+}
+
+// Individual saving functions
+fn C.IMG_SavePNG(surface &C.SDL_Surface, const_file &char) bool
+pub fn save_png(surface &sdl.Surface, const_file &char) bool {
+ return C.IMG_SavePNG(surface, const_file)
+}
+
+fn C.IMG_SavePNG_IO(surface &C.SDL_Surface, dst &C.SDL_IOStream, closeio bool) bool
+pub fn save_png_io(surface &sdl.Surface, dst &sdl.RWops, closeio bool) bool {
+ return C.IMG_SavePNG_IO(surface, dst, closeio)
+}
+
+fn C.IMG_SaveJPG(surface &C.SDL_Surface, const_file &char, quality int) bool
+pub fn save_jpg(surface &sdl.Surface, const_file &char, quality int) bool {
+ return C.IMG_SaveJPG(surface, const_file, quality)
+}
+
+fn C.IMG_SaveJPG_IO(surface &C.SDL_Surface, dst &C.SDL_IOStream, closeio bool, quality int) bool
+pub fn save_jpg_io(surface &sdl.Surface, dst &sdl.RWops, closeio bool, quality int) bool {
+ return C.IMG_SaveJPG_IO(surface, dst, closeio, quality)
+}
diff --git a/iostream.c.v b/iostream.c.v
index 9642f941..8504dc75 100644
--- a/iostream.c.v
+++ b/iostream.c.v
@@ -72,6 +72,9 @@ pub struct C.SDL_IOStream {
pub type IOStream = C.SDL_IOStream
+// RWops is kept for SDL2-era APIs and maps to SDL_IOStream in SDL3.
+pub type RWops = C.SDL_IOStream
+
// C.SDL_IOFromFile [official documentation](https://wiki.libsdl.org/SDL3/SDL_IOFromFile)
fn C.SDL_IOFromFile(const_file &char, const_mode &char) &IOStream
diff --git a/mixer/c.c.v b/mixer/c.c.v
new file mode 100644
index 00000000..e89d2526
--- /dev/null
+++ b/mixer/c.c.v
@@ -0,0 +1,27 @@
+// Copyright(C) 2021 Lars Pontoppidan. All rights reserved.
+// Use of this source code is governed by an MIT license
+// that can be found in the LICENSE file.
+module mixer
+
+$if !windows {
+ // SDL libs are loaded dynamically from Java on Android
+ $if !android || termux {
+ // sdl_no_compile_flags allow users to provide
+ // custom flags (e.g. via CFLAGS/LDFLAGS) for the compiler.
+ // This is especially useful when building/linking against a
+ // custom compiled version of the libs on *nix.
+ $if !sdl_no_compile_flags ? {
+ #flag -lSDL3_mixer
+ }
+ }
+}
+
+$if x64 {
+ #flag windows -L @VMODROOT/thirdparty/SDL3_mixer-3.1.2/lib/x64
+} $else {
+ #flag windows -L @VMODROOT/thirdparty/SDL3_mixer-3.1.2/lib/x86
+}
+#flag windows -I @VMODROOT/thirdparty/SDL3_mixer-3.1.2/include
+#flag windows -lSDL3_mixer
+
+#include
diff --git a/mixer/mixer.c.v b/mixer/mixer.c.v
new file mode 100644
index 00000000..0c07fce6
--- /dev/null
+++ b/mixer/mixer.c.v
@@ -0,0 +1,159 @@
+// Copyright(C) 2026 Lars Pontoppidan. All rights reserved.
+// Use of this source code is governed by an MIT license
+// that can be found in the LICENSE file.
+module mixer
+
+//
+// SDL_mixer.h (SDL3)
+//
+import sdl
+
+pub const major_version = C.SDL_MIXER_MAJOR_VERSION
+pub const minor_version = C.SDL_MIXER_MINOR_VERSION
+pub const micro_version = C.SDL_MIXER_MICRO_VERSION
+
+// compiledversion is the version number macro for the current SDL_mixer version.
+pub fn compiledversion() int {
+ return C.SDL_VERSIONNUM(major_version, minor_version, micro_version)
+}
+
+fn C.MIX_Version() int
+
+// version gets the version of the dynamically linked SDL_mixer library.
+pub fn version() int {
+ return C.MIX_Version()
+}
+
+fn C.MIX_Init() bool
+
+// init initializes SDL_mixer.
+pub fn init() bool {
+ return C.MIX_Init()
+}
+
+fn C.MIX_Quit()
+
+// quit de-initializes SDL_mixer.
+pub fn quit() {
+ C.MIX_Quit()
+}
+
+@[noinit; typedef]
+pub struct C.MIX_Mixer {}
+
+pub type Mixer = C.MIX_Mixer
+
+@[noinit; typedef]
+pub struct C.MIX_Audio {}
+
+pub type Audio = C.MIX_Audio
+
+@[noinit; typedef]
+pub struct C.MIX_Track {}
+
+pub type Track = C.MIX_Track
+
+fn C.MIX_CreateMixerDevice(devid sdl.AudioDeviceID, const_spec &sdl.AudioSpec) &C.MIX_Mixer
+
+// create_mixer_device creates a mixer that plays sound directly to an audio device.
+pub fn create_mixer_device(devid sdl.AudioDeviceID, const_spec &sdl.AudioSpec) &Mixer {
+ return C.MIX_CreateMixerDevice(devid, const_spec)
+}
+
+fn C.MIX_DestroyMixer(mixer &C.MIX_Mixer)
+
+// destroy_mixer frees a mixer and closes its audio device, if any.
+pub fn destroy_mixer(mixer &Mixer) {
+ C.MIX_DestroyMixer(mixer)
+}
+
+fn C.MIX_LoadAudio(mixer &C.MIX_Mixer, const_path &char, predecode bool) &C.MIX_Audio
+
+// load_audio loads audio data from a filesystem path.
+pub fn load_audio(mixer &Mixer, const_path &char, predecode bool) &Audio {
+ return C.MIX_LoadAudio(mixer, const_path, predecode)
+}
+
+fn C.MIX_DestroyAudio(audio &C.MIX_Audio)
+
+// destroy_audio frees audio data previously loaded.
+pub fn destroy_audio(audio &Audio) {
+ C.MIX_DestroyAudio(audio)
+}
+
+fn C.MIX_CreateTrack(mixer &C.MIX_Mixer) &C.MIX_Track
+
+// create_track creates a track for mixing audio.
+pub fn create_track(mixer &Mixer) &Track {
+ return C.MIX_CreateTrack(mixer)
+}
+
+fn C.MIX_DestroyTrack(track &C.MIX_Track)
+
+// destroy_track frees a track.
+pub fn destroy_track(track &Track) {
+ C.MIX_DestroyTrack(track)
+}
+
+fn C.MIX_SetTrackAudio(track &C.MIX_Track, audio &C.MIX_Audio) bool
+
+// set_track_audio assigns audio data to a track.
+pub fn set_track_audio(track &Track, audio &Audio) bool {
+ return C.MIX_SetTrackAudio(track, audio)
+}
+
+fn C.MIX_PlayTrack(track &C.MIX_Track, options sdl.PropertiesID) bool
+
+// play_track starts (or restarts) mixing a track.
+pub fn play_track(track &Track, options sdl.PropertiesID) bool {
+ return C.MIX_PlayTrack(track, options)
+}
+
+fn C.MIX_PlayAudio(mixer &C.MIX_Mixer, audio &C.MIX_Audio) bool
+
+// play_audio plays audio in a fire-and-forget manner.
+pub fn play_audio(mixer &Mixer, audio &Audio) bool {
+ return C.MIX_PlayAudio(mixer, audio)
+}
+
+fn C.MIX_SetTrackLoops(track &C.MIX_Track, num_loops int) bool
+
+// set_track_loops changes the number of times a track will loop.
+pub fn set_track_loops(track &Track, num_loops int) bool {
+ return C.MIX_SetTrackLoops(track, num_loops)
+}
+
+fn C.MIX_PauseTrack(track &C.MIX_Track) bool
+
+// pause_track pauses a track.
+pub fn pause_track(track &Track) bool {
+ return C.MIX_PauseTrack(track)
+}
+
+fn C.MIX_ResumeTrack(track &C.MIX_Track) bool
+
+// resume_track resumes a paused track.
+pub fn resume_track(track &Track) bool {
+ return C.MIX_ResumeTrack(track)
+}
+
+fn C.MIX_StopTrack(track &C.MIX_Track, fade_out_frames i64) bool
+
+// stop_track halts a track, optionally fading out over time.
+pub fn stop_track(track &Track, fade_out_frames i64) bool {
+ return C.MIX_StopTrack(track, fade_out_frames)
+}
+
+fn C.MIX_TrackPlaying(track &C.MIX_Track) bool
+
+// track_playing reports whether a track is playing.
+pub fn track_playing(track &Track) bool {
+ return C.MIX_TrackPlaying(track)
+}
+
+fn C.MIX_SetTrackGain(track &C.MIX_Track, gain f32) bool
+
+// set_track_gain sets track gain; 1.0 is full volume.
+pub fn set_track_gain(track &Track, gain f32) bool {
+ return C.MIX_SetTrackGain(track, gain)
+}
diff --git a/ttf/c.c.v b/ttf/c.c.v
new file mode 100644
index 00000000..14629ad4
--- /dev/null
+++ b/ttf/c.c.v
@@ -0,0 +1,28 @@
+// Copyright(C) 2021 Lars Pontoppidan. All rights reserved.
+// Use of this source code is governed by an MIT license
+// that can be found in the LICENSE file.
+module ttf
+
+$if !windows {
+ // SDL libs are loaded dynamically from Java on Android
+ $if !android || termux {
+ // sdl_no_compile_flags allow users to provide
+ // custom flags (e.g. via CFLAGS/LDFLAGS) for the compiler.
+ // This is especially useful when building/linking against a
+ // custom compiled version of the libs on *nix.
+ $if !sdl_no_compile_flags ? {
+ #flag -lSDL3_ttf
+ }
+ }
+}
+
+$if x64 {
+ #flag windows -L @VMODROOT/thirdparty/SDL3_ttf-3.2.2/lib/x64
+} $else {
+ #flag windows -L @VMODROOT/thirdparty/SDL3_ttf-3.2.2/lib/x86
+}
+
+#flag windows -I @VMODROOT/thirdparty/SDL3_ttf-3.2.2/include
+#flag windows -lSDL3_ttf
+
+#include
diff --git a/ttf/ttf.c.v b/ttf/ttf.c.v
new file mode 100644
index 00000000..d0818915
--- /dev/null
+++ b/ttf/ttf.c.v
@@ -0,0 +1,84 @@
+// Copyright(C) 2026 Lars Pontoppidan. All rights reserved.
+// Use of this source code is governed by an MIT license
+// that can be found in the LICENSE file.
+module ttf
+
+//
+// SDL_ttf.h (SDL3)
+//
+import sdl
+
+pub const major_version = C.SDL_TTF_MAJOR_VERSION
+pub const minor_version = C.SDL_TTF_MINOR_VERSION
+pub const micro_version = C.SDL_TTF_MICRO_VERSION
+
+// compiledversion is the version number macro for the current SDL_ttf version.
+pub fn compiledversion() int {
+ return C.SDL_VERSIONNUM(major_version, minor_version, micro_version)
+}
+
+fn C.SDL_TTF_VERSION_ATLEAST(x int, y int, z int) bool
+
+// ttf_version_atleast evaluates to true if compiled with SDL_ttf at least X.Y.Z.
+pub fn ttf_version_atleast(x int, y int, z int) bool {
+ return C.SDL_TTF_VERSION_ATLEAST(x, y, z)
+}
+
+fn C.TTF_Version() int
+
+// version gets the version of the dynamically linked SDL_ttf library.
+pub fn version() int {
+ return C.TTF_Version()
+}
+
+@[typedef]
+pub struct C.TTF_Font {}
+
+pub type Font = C.TTF_Font
+
+fn C.TTF_Init() bool
+
+// init initializes the TTF engine.
+pub fn init() bool {
+ return C.TTF_Init()
+}
+
+// init_ttf is kept for compatibility with older code.
+pub fn init_ttf() bool {
+ return C.TTF_Init()
+}
+
+fn C.TTF_OpenFont(file &char, ptsize f32) &C.TTF_Font
+
+// open_font opens a font file and creates a font of the specified point size.
+pub fn open_font(file &char, ptsize f32) &Font {
+ return C.TTF_OpenFont(file, ptsize)
+}
+
+fn C.TTF_OpenFontIO(src &C.SDL_IOStream, closeio bool, ptsize f32) &C.TTF_Font
+
+// open_font_io opens a font from an SDL_IOStream.
+pub fn open_font_io(src &sdl.IOStream, closeio bool, ptsize f32) &Font {
+ return C.TTF_OpenFontIO(src, closeio, ptsize)
+}
+
+fn C.TTF_RenderText_Solid(font &C.TTF_Font, text &char, length usize, fg C.SDL_Color) &C.SDL_Surface
+
+// render_text_solid creates a surface and renders the given UTF-8 text.
+pub fn render_text_solid(font &Font, text string, fg sdl.Color) &sdl.Surface {
+ return C.TTF_RenderText_Solid(font, text.str, usize(text.len), fg)
+}
+
+fn C.TTF_CloseFont(font &C.TTF_Font)
+
+// close_font closes an opened font file.
+pub fn close_font(font &Font) {
+ C.TTF_CloseFont(font)
+}
+
+fn C.TTF_Quit()
+
+// quit de-initializes the TTF engine.
+pub fn quit() {
+ C.TTF_Quit()
+}
diff --git a/windows_install_dependencies.vsh b/windows_install_dependencies.vsh
index 888d7da6..3fdb47f3 100644
--- a/windows_install_dependencies.vsh
+++ b/windows_install_dependencies.vsh
@@ -7,8 +7,8 @@ const is_terminal = os.is_atty(1) > 0
const urls = [
'https://www.libsdl.org/release/SDL3-devel-3.4.0-VC.zip',
'https://www.libsdl.org/projects/SDL_image/release/SDL3_image-devel-3.4.0-VC.zip',
- // 'https://www.libsdl.org/projects/SDL_ttf/release/SDL3_ttf-devel-2.0.15-VC.zip',
- // 'https://www.libsdl.org/projects/SDL_mixer/release/SDL3_mixer-devel-2.0.4-VC.zip',
+ 'https://www.libsdl.org/projects/SDL_mixer/release/SDL3_mixer-devel-3.1.2-VC.zip',
+ 'https://www.libsdl.org/projects/SDL_ttf/release/SDL3_ttf-devel-3.2.2-VC.zip',
]
fn main() {