├── .github └── workflows │ ├── ci.yml │ └── main.yml ├── .gitignore ├── .gitmodules ├── Cargo.lock ├── Cargo.toml ├── README.md ├── UNLICENSE ├── default.nix ├── examples ├── c │ ├── build.sh │ └── cart.c ├── curlywas │ ├── control.cwa │ ├── cracklebass.cwa │ ├── cube_wireframe.cwa │ ├── fireworks.cwa │ ├── font_palette.cwa │ ├── line.cwa │ ├── plasma.cwa │ ├── plasma_chars.cwa │ ├── scaled_text.cwa │ ├── simple_music.cwa │ ├── skipahead.cwa │ ├── sprites.cwa │ ├── steadyon.cwa │ ├── technotunnel.cwa │ ├── tim_ges.cwa │ ├── trainride.cwa │ └── tunnel.cwa ├── include │ ├── microw8-api.cwa │ └── microw8-api.wat ├── rust │ ├── build.sh │ ├── readme.txt │ ├── tunnel.rs │ └── tunnel.uw8 ├── wat │ ├── tunnel_opt.wat │ └── xorscroll.wat └── zig │ ├── .gitignore │ ├── build.zig │ └── main.zig ├── logo.svg ├── platform ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── bin │ ├── loader.wasm │ └── platform.uw8 └── src │ ├── font.png │ ├── font.pxo │ ├── ges.cwa │ ├── ges_only.cwa │ ├── loader.cwa │ ├── main.rs │ └── platform.cwa ├── release ├── .gitignore └── make-release ├── site ├── .SRCINFO ├── .gitignore ├── config.toml ├── content │ ├── _index.md │ ├── docs.md │ └── versions.md ├── static │ ├── img │ │ ├── microw8.svg │ │ └── technotunnel.png │ ├── uw8 │ │ └── skipahead.uw8 │ ├── v0.1.0 │ │ └── index.html │ ├── v0.1.1 │ │ └── index.html │ ├── v0.1.2 │ │ └── index.html │ ├── v0.1pre1 │ │ └── index.html │ ├── v0.1pre2 │ │ └── index.html │ ├── v0.1pre3 │ │ └── index.html │ ├── v0.1pre4 │ │ └── index.html │ ├── v0.1pre5 │ │ └── index.html │ ├── v0.2.0-rc1 │ │ └── index.html │ ├── v0.2.0-rc2 │ │ └── index.html │ ├── v0.2.0-rc3 │ │ └── index.html │ ├── v0.2.0 │ │ └── index.html │ ├── v0.2.1 │ │ └── index.html │ ├── v0.2.2 │ │ └── index.html │ ├── v0.3.0 │ │ └── index.html │ ├── v0.4.0 │ │ └── index.html │ └── v0.4.1 │ │ └── index.html └── templates │ ├── _variables.html │ └── index.html ├── src ├── filewatcher.rs ├── lib.rs ├── main.rs ├── run-web.html ├── run_native.rs └── run_web.rs ├── syntax └── SublimeText │ ├── CurlyWASM.sublime-completions │ ├── CurlyWASM.sublime-syntax │ ├── data-block.sublime-snippet │ ├── fn-export.sublime-snippet │ ├── if-else.sublime-snippet │ ├── let-inline.sublime-snippet │ ├── let-lazy.sublime-snippet │ └── loop-branch_if.sublime-snippet ├── test ├── drawing_test.cwa ├── frame_time.cwa ├── ges_test.cwa ├── log.cwa ├── plot_ges.cwa ├── start_fn.cwa └── text_modes.cwa ├── todo.txt ├── uw8-tool ├── .gitignore ├── Cargo.lock ├── Cargo.toml └── src │ ├── base_module.rs │ ├── filter_exports.rs │ ├── lib.rs │ ├── main.rs │ └── pack.rs ├── uw8-window ├── .gitignore ├── Cargo.lock ├── Cargo.toml └── src │ ├── cpu.rs │ ├── gpu │ ├── crt.rs │ ├── crt.wgsl │ ├── fast_crt.rs │ ├── fast_crt.wgsl │ ├── mod.rs │ ├── palette.wgsl │ ├── scale_mode.rs │ ├── square.rs │ └── square.wgsl │ ├── lib.rs │ └── main.rs └── web ├── .gitignore ├── build-run-web ├── opus-repro.html ├── package.json ├── run ├── src ├── audiolet.js ├── index.html ├── main.js ├── microw8.js ├── run-web.css ├── run-web.html ├── run-web.js └── style.css └── yarn.lock /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | build: 12 | strategy: 13 | matrix: 14 | build: [ linux, windows, macos ] 15 | include: 16 | - build: linux 17 | os: ubuntu-latest 18 | exe: uw8 19 | - build: windows 20 | os: windows-latest 21 | exe: uw8.exe 22 | - build: macos 23 | os: macos-latest 24 | exe: uw8 25 | 26 | runs-on: ${{ matrix.os }} 27 | 28 | steps: 29 | - name: Install dependencies 30 | run: sudo apt-get install -y libxkbcommon-dev libasound2-dev 31 | if: matrix.os == 'ubuntu-latest' 32 | - name: Checkout 33 | uses: actions/checkout@v3 34 | - name: Cache build dirs 35 | uses: actions/cache@v3 36 | with: 37 | path: | 38 | ~/.cargo/bin/ 39 | ~/.cargo/registry/index/ 40 | ~/.cargo/registry/cache/ 41 | ~/.cargo/git/db/ 42 | target/ 43 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 44 | - name: Build 45 | run: cargo build --release --verbose 46 | - name: Upload artifact 47 | uses: actions/upload-artifact@v4 48 | with: 49 | name: uw8-${{ matrix.build }} 50 | path: target/release/${{ matrix.exe }} 51 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | name: Build and deploy GH Pages 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: checkout 11 | uses: actions/checkout@v3 12 | - name: build_and_deploy 13 | uses: shalzz/zola-deploy-action@70a101a14bbdeed13e7a42a9ed06b35c9e9e826e 14 | env: 15 | # Target branch 16 | PAGES_BRANCH: gh-pages 17 | BUILD_DIR: site 18 | # Provide personal access token 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .cargo/ 3 | .vscode/ 4 | /examples/**/*.wasm 5 | /examples/**/*.uw8 -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "site/themes/juice"] 2 | path = site/themes/juice 3 | url = https://github.com/huhu/juice 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "uw8" 3 | version = "0.4.1" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [features] 9 | default = ["native", "browser"] 10 | native = ["wasmtime", "uw8-window", "cpal", "rubato" ] 11 | browser = ["warp", "tokio", "tokio-stream", "webbrowser"] 12 | 13 | [dependencies] 14 | wasmtime = { git = "https://github.com/bytecodealliance/wasmtime.git", rev = "0f48f939b9870036562ca02ff21253547a9f1a5c", optional = true } 15 | anyhow = "1" 16 | env_logger = "0.11.3" 17 | log = "0.4" 18 | uw8-window = { path = "uw8-window", optional = true } 19 | notify-debouncer-mini = { version = "0.4.1", default-features = false } 20 | pico-args = "0.5" 21 | curlywas = { git = "https://github.com/exoticorn/curlywas.git", rev = "0e7ea50" } 22 | wat = "1" 23 | uw8-tool = { path = "uw8-tool" } 24 | same-file = "1" 25 | warp = { version = "0.3.6", optional = true } 26 | tokio = { version = "1.37.0", features = ["sync", "rt"], optional = true } 27 | tokio-stream = { version = "0.1.15", features = ["sync"], optional = true } 28 | webbrowser = { version = "0.8.13", optional = true } 29 | ansi_term = "0.12.1" 30 | cpal = { version = "0.15.3", optional = true } 31 | rubato = { version = "0.12.0", optional = true } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MicroW8 2 | 3 | MicroW8 is a WebAssembly based fantasy console inspired by the likes of [TIC-80](https://tic80.com/), [WASM-4](https://wasm4.org/) and [PICO-8](https://www.lexaloffle.com/pico-8.php). 4 | 5 | The initial motivation behind MicroW8 was to explore whether there was a way to make WebAssembly viable for size-coding. (Size coding being the art of creating tiny (often <= 256 bytes) graphical effects and games.) The available examples so far are all in this space, however, I very carefully made sure that all design decisions make sense from the point of view of bigger projects as well. 6 | 7 | See [here](https://exoticorn.github.io/microw8/) for more information and docs. 8 | 9 | ## Specs 10 | 11 | * Screen: 320x240, 256 colors, 60Hz 12 | * Modules: Up to 256KB (WASM) 13 | * Memory: 256KB 14 | * Gamepad input (D-Pad + 4 Buttons) 15 | 16 | ## Downloads 17 | 18 | * [Linux](https://github.com/exoticorn/microw8/releases/download/v0.4.1/microw8-0.4.1-linux.tgz) 19 | * [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.4.1/microw8-0.4.1-macos.tgz) 20 | * [Windows](https://github.com/exoticorn/microw8/releases/download/v0.4.1/microw8-0.4.1-windows.zip) 21 | 22 | The download includes 23 | 24 | * `microw8.html`: The web runtime, a small, self-contained html file that can be opened in any modern browser to load and run MicroW8 carts. 25 | * `uw8`/`uw8.exe`: The MicroW8 dev tool, including a native runtime. 26 | * `examples`: Example source code in CurlyWas and Wat (WebAssembly text format). 27 | * `carts`: The examples compiled to `.uw8` carts. 28 | 29 | ## uw8 dev tool 30 | 31 | ``` 32 | uw8 run [] 33 | 34 | Runs which can be a binary WebAssembly module, an `.uw8` cart, a wat (WebAssembly text format) source file or a CurlyWas source file. 35 | 36 | Options: 37 | 38 | -b, --browser : Run in browser instead of using native runtime 39 | -t, --timeout FRAMES : Sets the timeout in frames (1/60s) 40 | -w, --watch : Reloads the given file every time it changes on disk. 41 | -p, --pack : Pack the file into an .uw8 cart before running it and print the resulting size. 42 | -u, --uncompressed : Use the uncompressed uw8 format for packing. 43 | -l LEVEL, --level LEVEL : Compression level (0-9). Higher compression levels are really slow. 44 | -o FILE, --output FILE : Write the loaded and optionally packed cart back to disk. 45 | 46 | when using the native runtime: 47 | 48 | -m, --no-audio : Disable audio, also reduces cpu load a bit 49 | --no-gpu : Force old cpu-only window code 50 | --filter FILTER : Select an upscale filter at startup 51 | --fullscreen : Start in fullscreen mode 52 | 53 | Note that the cpu-only window does not support fullscreen nor upscale filters. 54 | 55 | Unless --no-gpu is given, uw8 will first try to open a gpu accelerated window, falling back to the old cpu-only window if that fails. 56 | Therefore you should rarely need to manually pass --no-gpu. If you prefer the old pixel doubling look to the now default crt filter, 57 | you can just pass "--filter nearest" or "--filter 1". 58 | 59 | The upscale filter options are: 60 | 1, nearest : Anti-aliased nearest filter 61 | 2, fast_crt : Very simple, cheap crt filter, not very good below a window size of 960x720 62 | 3, ss_crt : Super sampled crt filter, a little more demanding on the GPU but scales well to smaller window sizes 63 | 4, chromatic_crt : Variant of fast_crt with a slight offset of the three color dots of a pixel, still pretty cheap 64 | 5, auto_crt (default) : ss_crt below 960x720, chromatic_crt otherwise 65 | 66 | You can switch the upscale filter at any time using the keys 1-5. You can toggle fullscreen with F. 67 | 68 | uw8 pack [] 69 | 70 | Packs the WebAssembly module or text file, or CurlyWas source file into a .uw8 cart. 71 | 72 | Options: 73 | 74 | -u, --uncompressed : Use the uncompressed uw8 format for packing. 75 | -l LEVEL, --level LEVEL : Compression level (0-9). Higher compression levels are really slow. 76 | 77 | 78 | uw8 unpack 79 | 80 | Unpacks a MicroW8 module into a standard WebAssembly module. 81 | 82 | 83 | uw8 compile [] 84 | 85 | Compiles a CurlyWas source file to a standard WebAssembly module. Most useful together with 86 | the --debug option to get a module that works well in the Chrome debugger. 87 | 88 | Options: 89 | 90 | -d, --debug : Generate a name section to help debugging 91 | 92 | 93 | uw8 filter-exports 94 | 95 | Reads a binary WebAssembly module, removes all exports not used by the MicroW8 platform + everything that is unreachable without those exports and writes the resulting module to . 96 | ``` 97 | 98 | ## Examples 99 | 100 | * [Fireworks](https://exoticorn.github.io/microw8/v0.1pre5#AgwvgP+M59snqjl4CMKw5sqm1Zw9yJCbSviMjeLUdHus2a3yl/a99+uiBeqZgP/2jqSjrLjRk73COMM6OSLpsxK8ugT1kuk/q4hQUqqPpGozHoa0laulzGGcahzdfdJsYaK1sIdeIYS9M5PnJx/Wk9H+PvWEPy2Zvv7I6IW7Fg==) (127 bytes): Some fireworks to welcome 2022. 101 | * [Skip Ahead](https://exoticorn.github.io/microw8/v0.1pre5#AgyfpZ80wkW28kiUZ9VIK4v+RPnVxqjK1dz2BcDoNyQPsS2g4OgEzkTe6jyoAfFOmqKrS8SM2aRljBal9mjNn8i4fP9eBK+RehQKxxGtJa9FqftvqEnh3ez1YaYxqj7jgTdzJ/WAYVmKMovBT1myrX3FamqKSOgMsNedLhVTLAhQup3sNcYEjGNo8b0HZ5+AgMgCwYRGCe//XQOMAaAAzqDILgmpEZ/43RKHcQpHEQwbURfNQJpadJe2sz3q5FlQnTGXQ9oSMokidhlC+aR/IpNHieuBGLhFZ2GfnwVQ0geBbQpTPA==) (229 bytes): A port of my [TIC-80 256byte game](http://tic80.com/play?cart=1735) from LoveByte'21 102 | * [OhNoAnotherTunnel](https://exoticorn.github.io/microw8/v0.1pre4#Ag95rdCB5Ww5NofyQaKF4P1mrNRso4azgiem4hK99Gh8OMzSpFq3NsNDo7O7pqln10D11l9uXr/ritw7OEzKwbEfCdvaRnS2Z0Kz0iDEZt/gIqOdvFmxsL1MjPQ4XInPbUJpQUonhQq29oP2omFabnQxn0bzoK7mZjcwc5GetHG+hGajkJcRr8oOnjfCol8RD+ha33GYtPnut+GLe4ktzf5UxZwGs6oT9qqC61lRDakN) (177 bytes): A port of my [entry](http://tic80.com/play?cart=1871) in the Outline'21 bytebattle final 103 | * [Technotunnel](https://exoticorn.github.io/microw8/v0.1pre4#AqL8HeK1M9dn2nWNIF5vaq/Vh64pMt5nJIFoFKpBMPUsGtDtpqjo1JbT9LzPhAxCqJ7Yh4TA6oTGd4xhLowf+cWZMY73+7AZmfXJJsBi4cej/hH+4wlAgxFIrnOYnr/18IpnZbsHf0eGm1BhahX74+cVR0TRmNQmYC7GhCNS3mv/3MJn74lCj7t28aBJPjEZhP9fGXdG2u5Egh/Tjdg=) (158 bytes): A port of my [entry](https://tic80.com/play?cart=1873) in the Outline'21 bytebattle quater final 104 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | with import {}; 2 | stdenv.mkDerivation { 3 | name = "dev-environment"; # Probably put a more meaningful name here 4 | buildInputs = [ pkg-config libxkbcommon ]; 5 | } 6 | 7 | -------------------------------------------------------------------------------- /examples/c/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | clang -O2 -Wno-incompatible-library-redeclaration --no-standard-libraries -ffast-math -Xclang -target-feature -Xclang +nontrapping-fptoint -Wl,--no-entry,--export-all,--import-memory,--initial-memory=262144,--global-base=81920,-zstack-size=4096 -o cart.wasm cart.c --target=wasm32 && \ 4 | uw8 filter-exports cart.wasm cart.wasm && \ 5 | wasm-opt -Oz --fast-math --strip-producers -o cart.wasm cart.wasm && \ 6 | uw8 pack -l 9 cart.wasm cart.uw8 -------------------------------------------------------------------------------- /examples/c/cart.c: -------------------------------------------------------------------------------- 1 | #define IMPORT(MODULE, NAME) __attribute__((import_module(MODULE), import_name(NAME))) 2 | 3 | IMPORT("env", "atan2") extern float atan2(float, float); 4 | IMPORT("env", "time") extern float time(); 5 | 6 | float sqrt(float v) { 7 | return __builtin_sqrt(v); 8 | } 9 | 10 | #define FRAMEBUFFER ((unsigned char*)120) 11 | 12 | void upd() { 13 | int i = 0; 14 | 15 | for( ;; ) { 16 | float t = time() * 63.0f; 17 | float x = (float)(i % 320 - 160); 18 | float y = (float)(i / 320 - 120); 19 | float d = 40000.0f / sqrt(x * x + y * y); 20 | float u = atan2(x, y) * 512.0f / 3.141f; 21 | unsigned char c = (unsigned char)((int)(d + t * 2.0f) ^ (int)(u + t)) >> 4; 22 | FRAMEBUFFER[i] = c; 23 | 24 | i += 1; 25 | if(i >= 320*240) break; 26 | } 27 | } -------------------------------------------------------------------------------- /examples/curlywas/control.cwa: -------------------------------------------------------------------------------- 1 | include "../include/microw8-api.cwa" 2 | 3 | export fn upd() { 4 | printString(0x20000); 5 | } 6 | 7 | data 0x20000 { 8 | i8(14, 0xfd, 15, 15, 12) // clear screen to color 0xfd 9 | "Top left" 10 | i8(14, 10, 11, 11, 11, 11) // scroll up 4 lines 11 | i8(31, 28, 29, 14, 0xfd) "Bottom right" 12 | i8(14, 10, 10, 10) // scroll down 2 lines 13 | i8(31, 40, 3, 14, 10, 15, 0xf0) "Other colors" 14 | i8(24, 0xb0) "inverted" 15 | i8(13, 10, 8, 8) "->" 16 | i8(10, 10, 9, 9, 1) "|<-" 17 | i8(5, 31, 7, 28+17, 15, 0xe3) "Graphics text!" 18 | i8(5, 31, 6, 28+16, 15, 0xe5) "Graphics text!" 19 | i8(4, 24, 14, 10, 0x90, 0x80, 0xf1) 20 | i8(31, 37, 29, 0xf1, 0x80, 0x90) 21 | i8(0) 22 | } 23 | -------------------------------------------------------------------------------- /examples/curlywas/cracklebass.cwa: -------------------------------------------------------------------------------- 1 | // port of cracklebass by pestis (originally on TIC-80) 2 | 3 | include "../include/microw8-api.cwa" 4 | 5 | const MUSIC_DATA = 0x20000; 6 | 7 | export fn upd() { 8 | let inline t = 32!32 * 6 / 100; 9 | let inline p = t / 1024; 10 | 11 | let channel:i32; 12 | 13 | loop channels { 14 | let inline e = t * channel?MUSIC_DATA / 8; 15 | let lazy pattern = (8 * channel + p)?(MUSIC_DATA + 56); 16 | let lazy n = !!pattern * (8 * pattern + e / 16 % 8)?MUSIC_DATA; 17 | let inline prev_ctrl = (channel * 6)?80; 18 | (channel * 6)?80 = if n { 19 | let inline base_note = 12 + 12 * channel?(MUSIC_DATA + 4) + n; 20 | let inline pitch_drop = e % 16 * channel?(MUSIC_DATA + 94); 21 | let inline key_pattern = p?(MUSIC_DATA + 8*4 + 56); 22 | let inline key = select(key_pattern, (8 * key_pattern + t / 128 % 8)?MUSIC_DATA, 1); 23 | (channel * 6)?83 = base_note - pitch_drop / 4 + key; 24 | prev_ctrl & 0xfc | (e / 8 & 2) | 1 25 | } else { 26 | prev_ctrl & 0xfe 27 | }; 28 | 29 | branch_if (channel := channel + 1) < 4: channels; 30 | } 31 | } 32 | 33 | data 80 { 34 | i8( 35 | 0x44, 0, 0, 0, 0x50, 0x40, 36 | 0x4, 0x50, 0, 0, 0x80, 0x80, 37 | 0x40, 0x80, 0, 0, 0x40, 0x40, 38 | 0, 0, 0, 0, 0x50, 0x50 39 | ) 40 | } 41 | 42 | data MUSIC_DATA { 43 | i8( 44 | 16, 2, 8, 8, 1, 2, 2, 3, 1, 0, 45 | 1,13,16, 0, 1, 8, 1, 0, 1,13, 46 | 16, 1, 1, 8, 1, 0, 8,13,13, 0, 47 | 16,13, 1, 0, 1, 0, 1, 0, 1, 1, 48 | 1, 0, 0, 0, 1, 0,13, 1, 1, 1, 49 | 6, 8, 1, 1, 6, 8, 1, 1, 2, 1, 50 | 2, 1, 2, 0, 0, 0, 0, 3, 3, 3, 51 | 5, 0, 0, 2, 1, 2, 1, 2, 1, 2, 52 | 0, 4, 4, 0, 4, 4, 4, 4, 0, 0, 53 | 0, 0, 6, 6, 0, 0, 0, 8 54 | ) 55 | } -------------------------------------------------------------------------------- /examples/curlywas/fireworks.cwa: -------------------------------------------------------------------------------- 1 | include "../include/microw8-api.cwa" 2 | 3 | export fn upd() { 4 | cls(0); 5 | 6 | let i: i32; 7 | loop pixels { 8 | let inline rocket = i #>> 9; 9 | let lazy local_time = fmod(time() + rocket as f32 / 5 as f32, 2 as f32); 10 | let lazy rocket = rocket + nearest(time() - local_time) as i32 * 10; 11 | randomSeed(rocket); 12 | let inline x = randomf() * 645 as f32; 13 | let y = randomf() * 133 as f32; 14 | let lazy angle = { randomSeed(i); randomf() } * 44 as f32; 15 | let inline dx = sin(angle); 16 | let inline dy = cos(angle); 17 | let lazy dist = local_time * (randomf() * 44 as f32); 18 | circle( 19 | x + dx * dist, 20 | y + dy * dist + local_time * local_time * 24 as f32, 21 | 1 as f32, (rocket % 11 + 1) * 16 - (local_time * 7 as f32) as i32 - (i % 4) 22 | ); 23 | branch_if (i := i + 1) < 5120: pixels; 24 | } 25 | } -------------------------------------------------------------------------------- /examples/curlywas/font_palette.cwa: -------------------------------------------------------------------------------- 1 | include "../include/microw8-api.cwa" 2 | 3 | global mut mode: i32 = 0; 4 | 5 | export fn upd() { 6 | cls(0); 7 | 8 | if isButtonTriggered(BUTTON_A) { 9 | mode = !mode; 10 | } 11 | 12 | setTextColor(15); 13 | printString(mode * USER_MEM); 14 | 15 | let y: i32; 16 | loop y { 17 | line(0 as f32, (y * 9 + 39) as f32, (14+16*9) as f32, (y * 9 + 39) as f32, 1); 18 | line((y * 9 + 15) as f32, 24 as f32, (y * 9 + 15) as f32, (38+16*9) as f32, 1); 19 | setTextColor(15); 20 | setCursorPosition(y * 9 + 16, 24); 21 | let lazy hexChar = select(y < 10, y + 48, y + 87); 22 | printChar(hexChar); 23 | setCursorPosition(0, y * 9 + 24+16); 24 | printChar(hexChar); 25 | let x = 0; 26 | loop x { 27 | setCursorPosition(x * 9 + 16, y * 9 + 24+16); 28 | setTextColor(select(mode, x + y * 16, -9)); 29 | if y >= 2 | mode { 30 | printChar(select(mode, 0xa4, x + y * 16)); 31 | } 32 | branch_if (x := x + 1) < 16: x; 33 | } 34 | branch_if (y := y + 1) < 16: y; 35 | } 36 | } 37 | 38 | data 0 { 39 | "Default font: (press " i8(0xcc) " for palette)" i8(5, 0) 40 | } 41 | 42 | data USER_MEM { 43 | "Default palette: (press " i8(0xcc) " for font)" i8(5, 0) 44 | } -------------------------------------------------------------------------------- /examples/curlywas/line.cwa: -------------------------------------------------------------------------------- 1 | include "../include/microw8-api.cwa" 2 | 3 | export fn upd() { 4 | cls(0); 5 | let i: i32; 6 | loop lines { 7 | let angle = i as f32 * (3.1415 / 25.0) + time() * 0.125; 8 | line( 9 | 160 as f32, 120 as f32, 10 | 160 as f32 + sin(angle) * 100 as f32, 11 | 120 as f32 + cos(angle) * 100 as f32, 12 | 47); 13 | branch_if (i := i + 1) < 50: lines; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/curlywas/plasma.cwa: -------------------------------------------------------------------------------- 1 | /* 2 | Plasma effect (sizecoding) 3 | 4 | Combines sine waves to create a 2D pixel pattern with intensity-based colors. 5 | 6 | code : zbyti & Grok 3 7 | date : 2025.04.17 8 | platform : MicroW8 0.4.1 9 | */ 10 | 11 | include "../include/microw8-api.cwa" 12 | 13 | // Constants for color, math, and screen dimensions 14 | const BASE_COLOR = 0xF0; // Base color value for pixel coloring 15 | const PI = 3.14159265; // Mathematical constant π 16 | const RAD = PI / 180.0; // Conversion factor from degrees to radians 17 | const SCR_X = 320; // Screen width in pixels 18 | const SCR_Y = 240; // Screen height in pixels 19 | const SCR_SIZE = SCR_X * SCR_Y; // Screen size in bytes 20 | 21 | // Global variables to track animation phases 22 | global mut phaseX = 0; // Phase offset for X-axis wave animation 23 | global mut phaseY = 0; // Phase offset for Y-axis wave animation 24 | 25 | // Update function called each frame to render the plasma effect 26 | export fn upd() { 27 | let i = 0; 28 | loop i { 29 | // Calculate pixel coordinates from linear index 30 | let lazy x = i % SCR_X; // X-coordinate (column) 31 | let lazy y = i / SCR_X; // Y-coordinate (row) 32 | 33 | // Compute three sine waves with different frequencies and phases 34 | let inline val1 = sin(RAD * 2.25 * (x + phaseX) as f32); // Wave along X-axis 35 | let inline val2 = sin(RAD * 3.25 * (y + phaseY) as f32); // Wave along Y-axis 36 | let inline val3 = sin(RAD * 1.25 * (x + y + phaseX) as f32); // Diagonal wave 37 | 38 | // Combine waves, scale to color range, and convert to integer 39 | let inline c = BASE_COLOR + ((val1 + val2 + val3) * 4.75) as i32; 40 | 41 | // Set pixel color based on computed intensity 42 | setPixel(x, y, c); 43 | 44 | // Continue loop until all pixels are processed 45 | branch_if (i +:= 1) < (SCR_SIZE): i; 46 | } 47 | 48 | // Update phase offsets for animation (different speeds for dynamic effect) 49 | phaseX += 1; // Increment X-phase for horizontal wave movement 50 | phaseY += 2; // Increment Y-phase for vertical wave movement 51 | } -------------------------------------------------------------------------------- /examples/curlywas/plasma_chars.cwa: -------------------------------------------------------------------------------- 1 | /* 2 | Plasma effect (chars) 3 | 4 | Combines sine waves to create a 2D pattern, maps to custom characters, 5 | and renders to the 40x30 character grid with intensity-based colors. 6 | 7 | code : zbyti (conversion & optimizations) 8 | original : https://github.com/tebe6502/Mad-Pascal/blob/origin/samples/a8/demoeffects/plasma_2.pas 9 | date : 2025.04.16 10 | platform : MicroW8 0.4.1 11 | */ 12 | 13 | include "../include/microw8-api.cwa" 14 | 15 | //----------------------------------------------------------------------------- 16 | // Constants defining memory layout, screen dimensions, and effect parameters 17 | //----------------------------------------------------------------------------- 18 | 19 | const MEM_END = 0x40000; 20 | 21 | const CUSTOM_FONT = FONT + 0x100; 22 | const CHARS = 0x20; 23 | const BASE_COLOR = 0xF0; 24 | 25 | const PI = 3.14159265; 26 | const RAD = PI / 180.0; 27 | 28 | const SCR_X = 320; 29 | const SCR_Y = 240; 30 | const SCR_W = SCR_X / 8; // 40 31 | const SCR_H = SCR_Y / 8; // 30 32 | const SCR_SIZE = SCR_X * SCR_Y; 33 | 34 | const SIN_TABLE_SIZE = 128; // Number of entries in the sine lookup table 35 | const SIN_TABLE_MASK = SIN_TABLE_SIZE - 1; 36 | 37 | const SIN_TABLE = MEM_END - SIN_TABLE_SIZE; // Memory address for sine table 38 | const ROW_BUFFER = SIN_TABLE - SCR_W; // Memory address for precomputed row buffer 39 | 40 | //----------------------------------------------------------------------------- 41 | // Global variables to track animation state 42 | //----------------------------------------------------------------------------- 43 | 44 | global mut phaseA = 1; // Phase offset for the first sine wave, controls animation 45 | global mut phaseB = 5; // Phase offset for the second sine wave, controls animation 46 | 47 | //----------------------------------------------------------------------------- 48 | 49 | /* 50 | Logs a value to the console (STDOUT) with a prefix for debugging purposes 51 | 52 | Args: 53 | prefix : 4-character identifier (i32) to label the output 54 | log : Integer value to log (i32) 55 | 56 | Prints to console and returns to screen output 57 | */ 58 | fn console(prefix: i32, log: i32) { 59 | printChar('\6'); // Switch output to console 60 | printChar(prefix); // Print the prefix 61 | printInt(log); // Print the integer value 62 | printChar('\n\4'); // Print newline and switch back to screen output 63 | } 64 | 65 | //------------------------------------- 66 | 67 | /* 68 | Fills a sine table with precomputed values for fast lookups 69 | 70 | Args: 71 | adr : Memory address where the sine table is stored 72 | size : Number of entries in the table 73 | 74 | Computes: sin(i * 180/size * radians) * 255 for i = 0 to size-1, scaled to 0-255 75 | */ 76 | fn fillSin(adr: i32, size: i32) { 77 | let i = 0; 78 | let inline f = 180.00 / size as f32 * RAD; 79 | loop i { 80 | (adr+i)?0 = (sin(f * i as f32) * 255.0) as i32; 81 | branch_if (i +:= 1) < size: i; 82 | } 83 | } 84 | 85 | //----------------------------------------------------------------------------- 86 | 87 | /* 88 | Initialization function called when the program starts 89 | */ 90 | export fn start() { 91 | // Populate the sine table with values for fast wave calculations 92 | fillSin(SIN_TABLE, SIN_TABLE_SIZE); 93 | } 94 | 95 | //------------------------------------- 96 | 97 | /* 98 | Update function called each frame to render the plasma effect 99 | */ 100 | export fn upd() { 101 | let pA = phaseA; // Local copy of phaseA to avoid modifying global during frame 102 | let pB = phaseB; // Local copy of phaseB for the same reason 103 | 104 | let i = 0; 105 | loop i { 106 | // Wrap phase values to stay within sine table bounds using bitwise AND 107 | pA &= SIN_TABLE_MASK; 108 | pB &= SIN_TABLE_MASK; 109 | 110 | // Combine two sine waves and store in row buffer 111 | i?ROW_BUFFER = pA?SIN_TABLE + pB?SIN_TABLE; 112 | 113 | pA += 3; // Shift phase for first sine wave (controls wave speed) 114 | pB += 7; // Shift phase for second sine wave (different speed for variety) 115 | 116 | branch_if (i +:= 1) < SCR_W: i; 117 | } 118 | 119 | i = 0; 120 | loop i { 121 | let j = 0; 122 | loop j { 123 | // Combine wave values from row buffer for current position 124 | // Use bitwise AND to clamp to 0-255, then scale to 0-15 for character index 125 | let c = ((j?ROW_BUFFER + i?ROW_BUFFER) & 255) >> 4; 126 | 127 | // Set text color based on intensity (base color + scaled value) 128 | setTextColor(BASE_COLOR + c); 129 | // Draw custom character corresponding to intensity 130 | printChar(CHARS + c); 131 | 132 | branch_if (j +:= 1) < SCR_W: j; 133 | } 134 | branch_if (i +:= 1) < SCR_H: i; 135 | } 136 | 137 | // Update global phase offsets for the next frame to animate the pattern 138 | phaseA += 0; // Increment phaseA to shift the pattern (+/- speed move) 139 | phaseB += 1; // Increment phaseB to shift the pattern (+/- right/left move) 140 | } 141 | 142 | //----------------------------------------------------------------------------- 143 | 144 | /* 145 | Custom font data defining 16 characters (8x8 pixels each) 146 | Each character represents a different intensity level for the plasma effect 147 | Pixels range from empty (0x00) to nearly full (0xFE or 0x7F) to simulate gradients 148 | */ 149 | data CUSTOM_FONT { 150 | i8( 151 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 152 | 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 153 | 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 154 | 0x00, 0x00, 0x38, 0x38, 0x38, 0x00, 0x00, 0x00, 155 | 0x00, 0x00, 0x3c, 0x3c, 0x3c, 0x3c, 0x00, 0x00, 156 | 0x00, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x00, 0x00, 157 | 0x00, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x00, 158 | 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0x00, 159 | 0x00, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 160 | 0x00, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x00, 161 | 0x00, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x00, 0x00, 162 | 0x00, 0x00, 0x3c, 0x3c, 0x3c, 0x3c, 0x00, 0x00, 163 | 0x00, 0x00, 0x38, 0x38, 0x38, 0x00, 0x00, 0x00, 164 | 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 165 | 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 166 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 167 | ) 168 | } 169 | -------------------------------------------------------------------------------- /examples/curlywas/scaled_text.cwa: -------------------------------------------------------------------------------- 1 | include "../include/microw8-api.cwa" 2 | 3 | export fn upd() { 4 | printString(USER_MEM); 5 | } 6 | 7 | data USER_MEM { 8 | // clear screen, switch to graphics text mode, text scale 4 9 | i8(12, 5, 30, 4) 10 | // text color, position, print two lines 11 | i8(15, 86, 31, 8, 80) "Hello," i8(8, 8, 8, 8, 8, 10) "MicroW8!" 12 | // print same two lines with different color and slight offset 13 | i8(15, 47, 31, 10, 82) "Hello," i8(8, 8, 8, 8, 8, 10) "MicroW8!" i8(0) 14 | } 15 | -------------------------------------------------------------------------------- /examples/curlywas/simple_music.cwa: -------------------------------------------------------------------------------- 1 | include "../include/microw8-api.cwa" 2 | 3 | global mut frame = 0; 4 | 5 | export fn upd() { 6 | if frame % 16 == 0 { 7 | let ch: i32; 8 | loop channels { 9 | playNote(ch, (ch * 32 + (frame / 16) % 32)?0x20000); 10 | branch_if ch := (ch + 1) % 4: channels; 11 | } 12 | } 13 | frame = frame + 1; 14 | } 15 | 16 | data 0x20000 { 17 | i8( 18 | 0x4e, 0x0, 0x0, 0x4c, 0x49, 0x0, 0x45, 0x47, 19 | 0x49, 0x47, 0x45, 0x44, 0x42, 0x0, 0x3d, 0x41, 20 | 0x44, 0x0, 0x0, 0x47, 0x49, 0x47, 0x45, 0x41, 21 | 0x44, 0x0, 0x0, 0x0, 0x42, 0x0, 0x0, 0x0, 22 | 23 | 0x25, 0, 0x49, 0x25, 0x25, 0, 0x49, 0x38, 24 | 0x25, 0, 0x49, 0x25, 0x25, 0, 0x49, 0x38, 25 | 0x25, 0, 0x49, 0x25, 0x25, 0, 0x49, 0x38, 26 | 0x25, 0, 0x49, 0x25, 0x25, 0, 0x49, 0x38, 27 | 28 | 0x2a, 0x0, 0x0, 0x0, 0x2d, 0x0, 0x0, 0x0, 29 | 0x2c, 0x0, 0x28, 0x0, 0x2a, 0x0, 0x0, 0x0, 30 | 0x25, 0x0, 0x0, 0x0, 0x29, 0x0, 0x0, 0x0, 31 | 0x2c, 0x0, 0x2d, 0x0, 0x2a, 0x0, 0x25, 0x0, 32 | 33 | 0x0, 0x0, 0x31, 0x0, 0x34, 0x0, 0x0, 0x36, 34 | 0x38, 0x39, 0x38, 0x34, 0x36, 0x0, 0x0, 0x0, 35 | 0x0, 0x3d, 0x3b, 0x39, 0x38, 0x0, 0x0, 0x0, 36 | 0x0, 0x39, 0x38, 0x39, 0x38, 0x0, 0x36, 0x0 37 | ) 38 | } -------------------------------------------------------------------------------- /examples/curlywas/skipahead.cwa: -------------------------------------------------------------------------------- 1 | include "../include/microw8-api.cwa" 2 | 3 | global mut pz: i32 = 4; 4 | global mut px: f32 = 2.0; 5 | global mut py: f32 = 2.0; 6 | global mut s: f32 = 2.0; 7 | global mut f: f32 = 2.0; 8 | 9 | export fn upd() { 10 | let y: i32; 11 | let inline zero = 0_f; 12 | 13 | let lazy control_speed = 0.03125; 14 | s = s + 0.1875 - (f + control_speed) * isButtonPressed(4 <| cls(4)) as f32; 15 | f = f * 0.5625; 16 | 17 | printInt(pz); 18 | 19 | loop lines { 20 | let lazy z = (4000 / (y := y + 1) + pz) / 20; 21 | let lazy x = px - ({randomSeed(z); random()} >> 30) as f32; 22 | let lazy w = 9 as f32 / sqrt(z as f32); 23 | let lazy rx = 160 as f32 - (y as f32 * x); 24 | let inline rw = y as f32 * w; 25 | 26 | let inline c = (z & 1) * -2; 27 | let inline yf = y as f32; 28 | rectangle(rx, yf, rw, yf / 6 as f32, c + 1); 29 | rectangle(rx, yf, rw, 1 as f32, c - 4); 30 | 31 | if y == 180 & py > zero { 32 | if x > w | x < zero { 33 | 0?80 = 0xc3; 34 | 3?80 = 32; 35 | return; 36 | } 37 | py = zero; 38 | s = zero; 39 | f = 2 as f32; 40 | } 41 | 42 | branch_if y < 240: lines; 43 | } 44 | 45 | circle(160 as f32, 160 as f32 + py, 22 as f32, -28); 46 | circle((160 - 6) as f32, (160 - 6) as f32 + py, 6 as f32, -26); 47 | 48 | 0?86 = py < zero; 49 | 3?86 = 32 - py as i32; 50 | 51 | px = px + (isButtonPressed(3) - isButtonPressed(2)) as f32 * control_speed; 52 | py = py + s; 53 | pz = pz + 1; 54 | } 55 | -------------------------------------------------------------------------------- /examples/curlywas/sprites.cwa: -------------------------------------------------------------------------------- 1 | include "../include/microw8-api.cwa" 2 | 3 | const SPRITE = 0x20000; 4 | 5 | export fn upd() { 6 | cls(0); 7 | let t = time() / 2_f; 8 | let i: i32; 9 | loop spriteLoop { 10 | let inline x = sin(t * -1.3 + i as f32 * (3.141 / 30_f)) * 180_f + 160_f; 11 | let inline y = sin(t * 1.7 + i as f32 * (3.141 / 40_f)) * 140_f + 120_f; 12 | blitSprite(SPRITE, 16, x as i32, y as i32, (i & 3) * 0x200 + 0x100); 13 | branch_if (i +:= 1) < 100: spriteLoop; 14 | } 15 | } 16 | 17 | start fn start() { 18 | printChar('OO'); 19 | circle(8_f, 8_f, 6_f, 75); 20 | grabSprite(SPRITE, 16, 0, 0, 0); 21 | } 22 | -------------------------------------------------------------------------------- /examples/curlywas/steadyon.cwa: -------------------------------------------------------------------------------- 1 | // Steady On Tim, It's Only A Budget Game 2 | // by Gasman / Hooy-Program 3 | // ported to MicroW8 by exoticorn/icebird 4 | 5 | include "../include/microw8-api.cwa" 6 | 7 | fn melody(t: i32, T: i32) -> i32 { 8 | let inline riff_pos = abs(((T&31) - 16) as f32) as i32; 9 | let lazy shift = ((1-((T>>5)&3))%2-1) as f32 / 6 as f32; 10 | 11 | let inline note_count = 5 - (T >= 512); 12 | let inline octave = (riff_pos/5) as f32; 13 | let inline riff_note = 5514 >> (riff_pos % note_count * 4) & 15; 14 | let inline melody_freq = pow(2 as f32, shift + octave - (riff_note as f32 / 12 as f32)); 15 | let inline melody = (t as f32 * melody_freq) as i32 & 128; 16 | 17 | let inline arp_note = ((0x85>>((t>>12)%3*4)) & 15) - 1; 18 | let inline arp_freq = pow(2 as f32, shift + (arp_note as f32 / 12 as f32)); 19 | let inline arp_vol = (T >= 256) * (12-T%12); 20 | let inline arpeggio = ((t as f32 * arp_freq) as i32 & 128) * arp_vol / 12; 21 | 22 | melody + arpeggio 23 | } 24 | 25 | export fn snd(t: i32) -> f32 { 26 | let lazy T = t/10000; 27 | 28 | let inline mel_arp = melody(t, T)/3 + melody(t, T-3)/5; 29 | 30 | let inline bass_vol = (T >= 128) & (197 >> (T % 8)); 31 | let inline bass_freq = pow(2 as f32, (((T & 4) * ((T & 7) - 1)) as f32 / 24 as f32 - 5 as f32)); 32 | let inline bass = ((t as f32 * bass_freq) as i32 & 63) * bass_vol; 33 | 34 | let inline snare_ish = (random() & 31) * (8 - (T + 4) % 8) / 8; 35 | 36 | let inline sample = mel_arp + bass + snare_ish; 37 | sample as f32 / 255 as f32 38 | } 39 | -------------------------------------------------------------------------------- /examples/curlywas/technotunnel.cwa: -------------------------------------------------------------------------------- 1 | include "../include/microw8-api.cwa" 2 | 3 | export fn upd() { 4 | let x: i32; 5 | let y: i32; 6 | loop screen { 7 | let inline t = time() / 2 as f32; 8 | let lazy o = sin(t) * 0.75; 9 | let inline q = x as f32 - 160.5; 10 | let inline w = (y - 120) as f32; 11 | let lazy r = sqrt(q*q + w*w); 12 | let lazy z = q / r; 13 | let lazy s = z * o + sqrt(z * z * o * o + 1 as f32 - o * o); 14 | let inline q2 = (z * s - o) * 10 as f32 + t; 15 | let inline w2 = w / r * s * 10 as f32 + t; 16 | let inline s2 = s * 100 as f32 / r; 17 | let inline color = max( 18 | 0 as f32, 19 | ((q2 as i32 ^ w2 as i32 & ((s2 + time()) * 10 as f32) as i32) & 5) as f32 * 20 | (4 as f32 - s2) as f32 21 | ) as i32 - 32; 22 | setPixel(x, y, color); 23 | branch_if x := (x + 1) % 320: screen; 24 | branch_if y := (y + 1) % 320: screen; 25 | } 26 | } -------------------------------------------------------------------------------- /examples/curlywas/tim_ges.cwa: -------------------------------------------------------------------------------- 1 | // Steady On Tim, It's Only A Budget Game 2 | // original bytebeat by Gasman / Hooy-Program 3 | // ported to MicroW8/GES by exoticorn/icebird 4 | 5 | import "env.memory" memory(4); 6 | 7 | fn melody(ch: i32, t: i32, T: i32) { 8 | let lazy riff_pos = abs(((T&31) - 16) as f32) as i32; 9 | let lazy shift = ((1-((T>>5)&3))%2-1) * 2; 10 | 11 | let inline note_count = 5 - (T >= 512); 12 | let inline octave = (riff_pos/5) * 12; 13 | let inline riff_note = 5514 >> (riff_pos % note_count * 4) & 15; 14 | let inline melody_note = shift + octave - riff_note; 15 | 16 | ch?1 = 230 - riff_pos * 14; 17 | ch?3 = melody_note + 64; 18 | 19 | let inline arp_note = shift + ((0x85>>((t/2)%3*4)) & 15) - 1; 20 | 80?3 = arp_note + 64; 21 | } 22 | 23 | export fn upd() { 24 | let lazy t = 32!32 / (1000/60); 25 | let lazy T = t / 7; 26 | melody(98, t, T - 3); 27 | melody(92, t, T); 28 | 29 | 80?0 = ((T >= 256) & (T/12+(T-3)/12)) * 2 | 0x48; // arp trigger 30 | 31 | if T >= 128 { 32 | let inline bass_step = T % 8; 33 | 86?3 = if bass_step / 2 == 2 { 34 | 86?0 = 0xd6; 35 | 81 36 | } else { 37 | 86?0 = ((197 >> bass_step) & 1) | 0x48; 38 | ((T & 4) * ((T & 7) - 1)) / 2 + 28 39 | }; 40 | } 41 | } 42 | 43 | data 80 { 44 | i8( 45 | 0, 0x90, 0, 0, 0, 0x90, 46 | 0, 0x4c, 0, 0, 0, 0x4c, 47 | 0x19, 0, 0, 0, 0, 0x4c, 48 | 0x19, 0, 0, 0, 0, 0x4c, 49 | 0xfa, 0x84, 50 | 0xc1, 0xc1, 0, 107, 0, 0x4c 51 | ) 52 | } 53 | 54 | /* 55 | include "../../platform/src/ges.cwa" 56 | 57 | import "env.pow" fn pow(f32, f32) -> f32; 58 | import "env.exp" fn exp(f32) -> f32; 59 | import "env.sin" fn sin(f32) -> f32; 60 | 61 | export fn snd(t: i32) -> f32 { 62 | gesSnd(t) 63 | } 64 | */ -------------------------------------------------------------------------------- /examples/curlywas/trainride.cwa: -------------------------------------------------------------------------------- 1 | include "../include/microw8-api.cwa" 2 | 3 | export fn upd() { 4 | let i: i32; 5 | loop pixels { 6 | let lazy x = (i % 320 - 160) as f32; 7 | let lazy y = (i / 320) as f32 - 120.5; 8 | let lazy z = time() + 20 as f32 / sqrt(x*x + y*y); 9 | let inline z_int = z as i32; 10 | let lazy q = select(z_int % 9 >= 6, z, (z_int - (z_int % 9 - 6)) as f32); 11 | let lazy w = 9 as f32 / y + time(); 12 | let inline s = q - time(); 13 | let inline m = x * s / 50 as f32; 14 | 15 | i?120 = select(y > 0 as f32 & w < q, 16 | select(abs(x * (w - time())) < 9 as f32, -2, -18) - w as i32 % 2, 17 | select(y * s > -99 as f32 / (m * m + 1 as f32), 18 | select(z_int % 9 >= 6, z_int % 2 - 31, -27), 19 | (-10 as f32 + y / 23 as f32 + fmod(y / 4 as f32, 1 as f32)) as i32 20 | ) 21 | ); 22 | branch_if (i := i + 1) < 320*240: pixels; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/curlywas/tunnel.cwa: -------------------------------------------------------------------------------- 1 | include "../include/microw8-api.cwa" 2 | 3 | export fn upd() { 4 | let i: i32; 5 | loop pixels { 6 | let inline t = 16!56; 7 | let inline x = (i % 320 - 160) as f32; 8 | let inline y = (i #/ 320 - 120) as f32; 9 | let inline d = 0xa000 as f32 / sqrt(x * x + y * y); 10 | let inline a = atan2(x, y) * 163_f; // (512 / pi) 11 | let inline u = i32.trunc_sat_f32_s(a) + t; 12 | let inline v = i32.trunc_sat_f32_s(d) + t * 2; 13 | let inline c = ((v ^ u) #/ 16) % 16; 14 | i?FRAMEBUFFER = c; 15 | 16 | branch_if (i := i + 1) < 320*240: pixels; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/include/microw8-api.cwa: -------------------------------------------------------------------------------- 1 | // MicroW8 APIs, to be `include`d in CurlyWas sources 2 | import "env.memory" memory(4); 3 | 4 | import "env.sin" fn sin(f32) -> f32; 5 | import "env.cos" fn cos(f32) -> f32; 6 | import "env.tan" fn tan(f32) -> f32; 7 | import "env.asin" fn asin(f32) -> f32; 8 | import "env.acos" fn acos(f32) -> f32; 9 | import "env.atan" fn atan(f32) -> f32; 10 | import "env.atan2" fn atan2(f32, f32) -> f32; 11 | import "env.pow" fn pow(f32, f32) -> f32; 12 | import "env.log" fn log(f32) -> f32; 13 | import "env.fmod" fn fmod(f32, f32) -> f32; 14 | import "env.random" fn random() -> i32; 15 | import "env.randomf" fn randomf() -> f32; 16 | import "env.randomSeed" fn randomSeed(i32); 17 | import "env.cls" fn cls(i32); 18 | import "env.setPixel" fn setPixel(i32, i32, i32); 19 | import "env.getPixel" fn getPixel(i32, i32) -> i32; 20 | import "env.hline" fn hline(i32, i32, i32, i32); 21 | import "env.rectangle" fn rectangle(f32, f32, f32, f32, i32); 22 | import "env.circle" fn circle(f32, f32, f32, i32); 23 | import "env.line" fn line(f32, f32, f32, f32, i32); 24 | import "env.time" fn time() -> f32; 25 | import "env.isButtonPressed" fn isButtonPressed(i32) -> i32; 26 | import "env.isButtonTriggered" fn isButtonTriggered(i32) -> i32; 27 | import "env.printChar" fn printChar(i32); 28 | import "env.printString" fn printString(i32); 29 | import "env.printInt" fn printInt(i32); 30 | import "env.setTextColor" fn setTextColor(i32); 31 | import "env.setBackgroundColor" fn setBackgroundColor(i32); 32 | import "env.setCursorPosition" fn setCursorPosition(i32, i32); 33 | import "env.rectangleOutline" fn rectangleOutline(f32, f32, f32, f32, i32); 34 | import "env.circleOutline" fn circleOutline(f32, f32, f32, i32); 35 | import "env.exp" fn exp(f32) -> f32; 36 | import "env.playNote" fn playNote(i32, i32); 37 | import "env.sndGes" fn sndGes(i32) -> f32; 38 | import "env.blitSprite" fn blitSprite(i32, i32, i32, i32, i32); 39 | import "env.grabSprite" fn grabSprite(i32, i32, i32, i32, i32); 40 | 41 | const TIME_MS = 0x40; 42 | const GAMEPAD = 0x44; 43 | const FRAMEBUFFER = 0x78; 44 | const PALETTE = 0x13000; 45 | const FONT = 0x13400; 46 | const USER_MEM = 0x14000; 47 | const BUTTON_UP = 0x0; 48 | const BUTTON_DOWN = 0x1; 49 | const BUTTON_LEFT = 0x2; 50 | const BUTTON_RIGHT = 0x3; 51 | const BUTTON_A = 0x4; 52 | const BUTTON_B = 0x5; 53 | const BUTTON_X = 0x6; 54 | const BUTTON_Y = 0x7; 55 | -------------------------------------------------------------------------------- /examples/include/microw8-api.wat: -------------------------------------------------------------------------------- 1 | ;; MicroW8 APIs, in WAT (Wasm Text) format 2 | (import "env" "memory" (memory 4)) 3 | 4 | (import "env" "sin" (func $sin (param f32) (result f32))) 5 | (import "env" "cos" (func $cos (param f32) (result f32))) 6 | (import "env" "tan" (func $tan (param f32) (result f32))) 7 | (import "env" "asin" (func $asin (param f32) (result f32))) 8 | (import "env" "acos" (func $acos (param f32) (result f32))) 9 | (import "env" "atan" (func $atan (param f32) (result f32))) 10 | (import "env" "atan2" (func $atan2 (param f32) (param f32) (result f32))) 11 | (import "env" "pow" (func $pow (param f32) (param f32) (result f32))) 12 | (import "env" "log" (func $log (param f32) (result f32))) 13 | (import "env" "fmod" (func $fmod (param f32) (param f32) (result f32))) 14 | (import "env" "random" (func $random (result i32))) 15 | (import "env" "randomf" (func $randomf (result f32))) 16 | (import "env" "randomSeed" (func $randomSeed (param i32))) 17 | (import "env" "cls" (func $cls (param i32))) 18 | (import "env" "setPixel" (func $setPixel (param i32) (param i32) (param i32))) 19 | (import "env" "getPixel" (func $getPixel (param i32) (param i32) (result i32))) 20 | (import "env" "hline" (func $hline (param i32) (param i32) (param i32) (param i32))) 21 | (import "env" "rectangle" (func $rectangle (param f32) (param f32) (param f32) (param f32) (param i32))) 22 | (import "env" "circle" (func $circle (param f32) (param f32) (param f32) (param i32))) 23 | (import "env" "line" (func $line (param f32) (param f32) (param f32) (param f32) (param i32))) 24 | (import "env" "time" (func $time (result f32))) 25 | (import "env" "isButtonPressed" (func $isButtonPressed (param i32) (result i32))) 26 | (import "env" "isButtonTriggered" (func $isButtonTriggered (param i32) (result i32))) 27 | (import "env" "printChar" (func $printChar (param i32))) 28 | (import "env" "printString" (func $printString (param i32))) 29 | (import "env" "printInt" (func $printInt (param i32))) 30 | (import "env" "setTextColor" (func $setTextColor (param i32))) 31 | (import "env" "setBackgroundColor" (func $setBackgroundColor (param i32))) 32 | (import "env" "setCursorPosition" (func $setCursorPosition (param i32) (param i32))) 33 | (import "env" "rectangleOutline" (func $rectangleOutline (param f32) (param f32) (param f32) (param f32) (param i32))) 34 | (import "env" "circleOutline" (func $circleOutline (param f32) (param f32) (param f32) (param i32))) 35 | (import "env" "exp" (func $exp (param f32) (result f32))) 36 | (import "env" "playNote" (func $playNote (param i32) (param i32))) 37 | (import "env" "sndGes" (func $sndGes (param i32) (result f32))) 38 | (import "env" "blitSprite" (func $blitSprite (param i32) (param i32) (param i32) (param i32) (param i32))) 39 | (import "env" "grabSprite" (func $grabSprite (param i32) (param i32) (param i32) (param i32) (param i32))) 40 | 41 | ;; to use defines, include this file with a preprocessor 42 | ;; like gpp (https://logological.org/gpp). 43 | #define TIME_MS 0x40; 44 | #define GAMEPAD 0x44; 45 | #define FRAMEBUFFER 0x78; 46 | #define PALETTE 0x13000; 47 | #define FONT 0x13400; 48 | #define USER_MEM 0x14000; 49 | #define BUTTON_UP 0x0; 50 | #define BUTTON_DOWN 0x1; 51 | #define BUTTON_LEFT 0x2; 52 | #define BUTTON_RIGHT 0x3; 53 | #define BUTTON_A 0x4; 54 | #define BUTTON_B 0x5; 55 | #define BUTTON_X 0x6; 56 | #define BUTTON_Y 0x7; 57 | -------------------------------------------------------------------------------- /examples/rust/build.sh: -------------------------------------------------------------------------------- 1 | rustc --target=wasm32-unknown-unknown -C target-feature=+nontrapping-fptoint --crate-type cdylib -C opt-level="z" -C "link-args=--import-memory --initial-memory=262144 -zstack-size=90000" -o tunnel.wasm tunnel.rs && \ 2 | uw8 filter-exports tunnel.wasm tunnel.wasm && \ 3 | wasm-opt -Oz --strip-producers -o tunnel.wasm tunnel.wasm && \ 4 | uw8 pack -l 9 tunnel.wasm tunnel.uw8 5 | -------------------------------------------------------------------------------- /examples/rust/readme.txt: -------------------------------------------------------------------------------- 1 | A small example how to produce somewhat reasonably small MicroW8 2 | carts in rust. 3 | 4 | A nightly rust compiler is needed for the unstable sqrtf32 5 | intrinsic. 6 | 7 | Simply compiling with rustc as shown in build.sh results in a 8 | 361 byte tunnel.wasm. Using wasm-opt this can be reduced to 9 | 255 bytes. 10 | 11 | When you disassemble this wasm file using wasm2wat you can see 12 | these globals and exports: 13 | 14 | (global (;0;) i32 (i32.const 90000)) 15 | (global (;1;) i32 (i32.const 90000)) 16 | (export "__data_end" (global 0)) 17 | (export "__heap_base" (global 1)) 18 | 19 | They are meant to be used for heap allocations and stack for any 20 | values that are not simple scalars (i32, f32, etc.). Since our 21 | code doesn't actually use any of that, the globals are only 22 | referenced by the exports and we can remove them using 23 | 'uw8 filter-exports' (preferably before running wasm-opt) which 24 | removes all exports except those used by the MicroW8 platform. 25 | 26 | This gives us a 211 byte wasm file. Running this through 27 | uw8 pack brings us to the final size of 119 bytes. -------------------------------------------------------------------------------- /examples/rust/tunnel.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![feature(core_intrinsics)] 3 | 4 | mod env { 5 | // "env" is the default module for imports, but it is still needed here 6 | // since there is a compiler builtin of the same name which is used 7 | // if we don't make it clear that this is a module import. 8 | #[link(wasm_import_module = "env")] 9 | extern "C" { 10 | pub fn atan2(x: f32, y: f32) -> f32; 11 | } 12 | 13 | extern "C" { 14 | pub fn time() -> f32; 15 | } 16 | } 17 | 18 | fn atan2(x: f32, y: f32) -> f32 { 19 | unsafe { env::atan2(x, y) } 20 | } 21 | 22 | fn time() -> f32 { 23 | unsafe { env::time() } 24 | } 25 | 26 | fn sqrt(v: f32) -> f32 { 27 | unsafe { core::intrinsics::sqrtf32(v) } 28 | } 29 | 30 | #[no_mangle] 31 | pub fn upd() { 32 | let mut i: i32 = 0; 33 | loop { 34 | let t = time() * 63.; 35 | let x = (i % 320 - 160) as f32; 36 | let y = (i / 320 - 120) as f32; 37 | let d = 40000 as f32 / sqrt(x * x + y * y); 38 | let u = atan2(x, y) * 512. / 3.141; 39 | let c = ((d + t * 2.) as i32 ^ (u + t) as i32) as u8 >> 4; 40 | unsafe { 41 | *((120 + i) as *mut u8) = c; 42 | } 43 | i += 1; 44 | if i >= 320*240 { 45 | break; 46 | } 47 | } 48 | } 49 | 50 | #[panic_handler] 51 | fn panic(_: &core::panic::PanicInfo) -> ! { 52 | loop {} 53 | } 54 | -------------------------------------------------------------------------------- /examples/rust/tunnel.uw8: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exoticorn/microw8/b9b2c2c927eabd54666079266d2abaf74218b946/examples/rust/tunnel.uw8 -------------------------------------------------------------------------------- /examples/wat/tunnel_opt.wat: -------------------------------------------------------------------------------- 1 | (module 2 | (import "env" "atan2" (func $atan2 (param f32 f32) (result f32))) 3 | (import "env" "time" (func $time (result f32))) 4 | (import "env" "memory" (memory 4)) 5 | (func (export "upd") 6 | (local $y i32) 7 | (local $i i32) 8 | (local $x i32) 9 | 10 | (loop $pixels 11 | i32.const 1 12 | local.get $i 13 | 14 | local.get $i 15 | 16 | i32.const 36928 17 | f32.convert_i32_s 18 | local.get $i 19 | i32.const 320 20 | i32.rem_s 21 | i32.const 160 22 | i32.sub 23 | local.tee $x 24 | local.get $x 25 | i32.mul 26 | local.get $i 27 | i32.const 320 28 | i32.div_s 29 | i32.const 120 30 | i32.sub 31 | local.tee $y 32 | local.get $y 33 | i32.mul 34 | i32.add 35 | f32.convert_i32_s 36 | f32.sqrt 37 | f32.div 38 | call $time 39 | i32.const 163 40 | f32.convert_i32_s 41 | f32.mul 42 | f32.add 43 | i32.trunc_sat_f32_s 44 | 45 | local.get $x 46 | f32.convert_i32_s 47 | local.get $y 48 | f32.convert_i32_s 49 | call $atan2 50 | i32.const 163 51 | f32.convert_i32_s 52 | f32.mul 53 | call $time 54 | i32.const 64 55 | f32.convert_i32_s 56 | f32.mul 57 | f32.add 58 | i32.trunc_f32_s 59 | 60 | i32.xor 61 | i32.const 4 62 | i32.shr_s 63 | i32.const 15 64 | i32.and 65 | i32.store8 offset=120 66 | 67 | i32.add 68 | local.tee $i 69 | i32.const 76800 70 | i32.rem_s 71 | br_if $pixels 72 | ) 73 | ) 74 | ) 75 | -------------------------------------------------------------------------------- /examples/wat/xorscroll.wat: -------------------------------------------------------------------------------- 1 | (module 2 | (import "env" "memory" (memory 4)) 3 | (func (export "upd") 4 | (local $i i32) ;; local variables are zero initialized 5 | 6 | (loop $pixels 7 | local.get $i ;; pixel index to write to 8 | 9 | (i32.rem_u (local.get $i) (i32.const 320)) ;; x 10 | (i32.div_u (i32.load (i32.const 64)) (i32.const 10)) ;; time / 10 11 | i32.add 12 | 13 | (i32.div_u (local.get $i) (i32.const 320)) ;; y 14 | 15 | i32.xor ;; (x + time / 10) ^ y 16 | (i32.shr_u (i32.const 3)) ;; .. >> 3 17 | (i32.and (i32.const 127)) ;; .. & 127 18 | 19 | i32.store8 offset=120 ;; store at pixel index + 120 20 | 21 | (i32.add (local.get $i) (i32.const 1)) ;; i + 1 22 | local.tee $i ;; write it back but keep it on the stack 23 | (br_if $pixels (i32.lt_s (i32.const 76800))) ;; branch to start of loop if i < 320*240 24 | ) 25 | ) 26 | ) 27 | -------------------------------------------------------------------------------- /examples/zig/.gitignore: -------------------------------------------------------------------------------- 1 | /zig-cache/ 2 | /zig-out/ -------------------------------------------------------------------------------- /examples/zig/build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.build.Builder) void { 4 | const mode = std.builtin.Mode.ReleaseSmall; 5 | 6 | const lib = b.addSharedLibrary("cart", "main.zig", .unversioned); 7 | lib.setBuildMode(mode); 8 | lib.setTarget(.{ 9 | .cpu_arch = .wasm32, 10 | .os_tag = .freestanding, 11 | .cpu_features_add = std.Target.wasm.featureSet(&.{ .nontrapping_fptoint }) 12 | }); 13 | lib.import_memory = true; 14 | lib.initial_memory = 262144; 15 | lib.max_memory = 262144; 16 | lib.global_base = 81920; 17 | lib.stack_size = 8192; 18 | lib.install(); 19 | 20 | if (lib.install_step) |install_step| { 21 | const run_filter_exports = b.addSystemCommand(&[_][]const u8{ 22 | "uw8", "filter-exports", "zig-out/lib/cart.wasm", "zig-out/lib/cart-filtered.wasm" 23 | }); 24 | run_filter_exports.step.dependOn(&install_step.step); 25 | 26 | const run_wasm_opt = b.addSystemCommand(&[_][]const u8{ 27 | "wasm-opt", "-Oz", "-o", "zig-out/cart.wasm", "zig-out/lib/cart-filtered.wasm" 28 | }); 29 | run_wasm_opt.step.dependOn(&run_filter_exports.step); 30 | 31 | const run_uw8_pack = b.addSystemCommand(&[_][]const u8{ 32 | "uw8", "pack", "-l", "9", "zig-out/cart.wasm", "zig-out/cart.uw8" 33 | }); 34 | run_uw8_pack.step.dependOn(&run_wasm_opt.step); 35 | 36 | const make_opt = b.step("make_opt", "make size optimized cart"); 37 | make_opt.dependOn(&run_uw8_pack.step); 38 | 39 | b.default_step = make_opt; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/zig/main.zig: -------------------------------------------------------------------------------- 1 | extern fn atan2(x: f32, y: f32) f32; 2 | extern fn time() f32; 3 | 4 | pub const FRAMEBUFFER: *[320*240]u8 = @intToPtr(*[320*240]u8, 120); 5 | 6 | export fn upd() void { 7 | var i: u32 = 0; 8 | while(true) { 9 | var t = time() * 63.0; 10 | var x = @intToFloat(f32, (@intCast(i32, i % 320) - 160)); 11 | var y = @intToFloat(f32, (@intCast(i32, i / 320) - 120)); 12 | var d = 40000.0 / @sqrt(x * x + y * y); 13 | var u = atan2(x, y) * 512.0 / 3.141; 14 | var c = @intCast(u8, (@floatToInt(i32, d + t * 2.0) ^ @floatToInt(i32, u + t)) & 255) >> 4; 15 | 16 | FRAMEBUFFER[@as(usize, i)] = c; 17 | i += 1; 18 | if(i >= 320*240) { break; } 19 | } 20 | } -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 41 | 48 | W8 58 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /platform/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ -------------------------------------------------------------------------------- /platform/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "platform" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | curlywas = { git="https://github.com/exoticorn/curlywas.git", rev="0e7ea50" } 10 | uw8-tool = { path="../uw8-tool" } 11 | anyhow = "1" 12 | lodepng = "3.7.2" -------------------------------------------------------------------------------- /platform/bin/loader.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exoticorn/microw8/b9b2c2c927eabd54666079266d2abaf74218b946/platform/bin/loader.wasm -------------------------------------------------------------------------------- /platform/bin/platform.uw8: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exoticorn/microw8/b9b2c2c927eabd54666079266d2abaf74218b946/platform/bin/platform.uw8 -------------------------------------------------------------------------------- /platform/src/font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exoticorn/microw8/b9b2c2c927eabd54666079266d2abaf74218b946/platform/src/font.png -------------------------------------------------------------------------------- /platform/src/font.pxo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exoticorn/microw8/b9b2c2c927eabd54666079266d2abaf74218b946/platform/src/font.pxo -------------------------------------------------------------------------------- /platform/src/ges_only.cwa: -------------------------------------------------------------------------------- 1 | import "env.memory" memory(1); 2 | import "env.sin" fn sin(f32) -> f32; 3 | import "env.pow" fn pow(f32, f32) -> f32; 4 | import "env.exp" fn exp(f32) -> f32; 5 | 6 | include "ges.cwa" -------------------------------------------------------------------------------- /platform/src/loader.cwa: -------------------------------------------------------------------------------- 1 | import "env.memory" memory(4); 2 | 3 | global mut base_end: i32 = 0; 4 | 5 | export fn load_uw8(module_size: i32) -> i32 { 6 | let lazy version = 0?0 - 1; 7 | if version < 0 { 8 | return module_size; 9 | } 10 | 11 | let module_end = 0x1e000 + module_size; 12 | if version & 1 { 13 | module_end = uncompress(1, 0x1e001); 14 | } else { 15 | copy(0x1e000, 0, module_size); 16 | } 17 | copy(0, 0x3c200, 8); 18 | 19 | let base_start = 0x3c208; 20 | let dest = 8; 21 | let src = 0x1e001; 22 | 23 | loop sections { 24 | if src < module_end & (base_start >= base_end | src?0 <= base_start?0) { 25 | let lazy length2 = copy_section(dest, src); 26 | dest = dest + length2; 27 | if base_start < base_end & src?0 == base_start?0 { 28 | base_start = base_start + section_size(base_start); 29 | } 30 | src = src + length2; 31 | branch sections; 32 | } 33 | 34 | if base_start < base_end { 35 | let lazy length3 = copy_section(dest, base_start); 36 | dest = dest + length3; 37 | base_start = base_start + length3; 38 | branch sections; 39 | } 40 | } 41 | 42 | dest 43 | } 44 | 45 | fn section_size(ptr: i32) -> i32 { 46 | let p = ptr; 47 | let l: i32; 48 | let shift: i32; 49 | loop size { 50 | let lazy b = (p := p + 1)?0; 51 | l = l | ((b & 127) << shift); 52 | shift = shift + 7; 53 | branch_if b >> 7: size; 54 | } 55 | p + 1 - ptr + l 56 | } 57 | 58 | fn copy_section(dest: i32, src: i32) -> i32 { 59 | let lazy length = section_size(src); 60 | copy(dest, src, length); 61 | length 62 | } 63 | 64 | fn copy(dest: i32, src: i32, len: i32) { 65 | loop bytes { 66 | if len > 0 { 67 | (dest + (len := len - 1))?0 = (src + len)?0; 68 | branch bytes; 69 | } 70 | } 71 | } 72 | 73 | // upkr unpacker 74 | 75 | global mut upkr_src_ptr: i32 = 0; 76 | global mut upkr_state: i32 = 0; 77 | 78 | // uncompress upkr compressed data at `src` into the buffer at `dest` 79 | // returns the end of the uncompressed data 80 | export fn uncompress(src_ptr: i32, dest_ptr: i32) -> i32 { 81 | upkr_src_ptr = src_ptr; 82 | upkr_state = 0; 83 | 84 | let offset: i32; 85 | 86 | let i: i32; 87 | loop init_contexts { 88 | i?0x3c000 = 0x80; 89 | branch_if (i := i + 1) < 256 + 1 + 128: init_contexts; 90 | } 91 | 92 | let prev_was_match: i32; 93 | 94 | block finished { 95 | loop unpack_loop { 96 | let lazy is_match = upkr_bit(0); 97 | if is_match { 98 | let inline new_offset = if prev_was_match { 1 } else { upkr_bit(256) }; 99 | if new_offset { 100 | branch_if !(offset := upkr_length(257) - 1): finished; 101 | } 102 | let length = upkr_length(257 + 64); 103 | loop copy { 104 | dest_ptr?0 = (dest_ptr - offset)?0; 105 | dest_ptr = dest_ptr + 1; 106 | branch_if (length := length - 1): copy; 107 | } 108 | } else { 109 | // literal 110 | let byte = 1; 111 | loop literal { 112 | branch_if (byte := (byte << 1) | upkr_bit(byte)) < 256: literal; 113 | } 114 | dest_ptr?0 = byte; 115 | dest_ptr = dest_ptr + 1; 116 | } 117 | prev_was_match = is_match; 118 | branch unpack_loop; 119 | } 120 | } 121 | 122 | dest_ptr 123 | } 124 | 125 | fn upkr_length(context_index: i32) -> i32 { 126 | let length: i32; 127 | let bit_pos: i32; 128 | loop bits { 129 | if upkr_bit(context_index + bit_pos) { 130 | length = length | (upkr_bit(context_index + bit_pos + 32) << bit_pos); 131 | bit_pos = bit_pos + 1; 132 | branch bits; 133 | } 134 | } 135 | length | (1 << bit_pos) 136 | } 137 | 138 | fn upkr_bit(context_index: i32) -> i32 { 139 | let lazy prob = context_index?0x3c000; 140 | 141 | loop refill { 142 | if upkr_state < 1<<12 { 143 | upkr_state = (upkr_state << 8) | upkr_src_ptr?0; 144 | upkr_src_ptr = upkr_src_ptr + 1; 145 | branch refill; 146 | } 147 | } 148 | 149 | let lazy state_low = upkr_state & 0xff; 150 | let lazy state_hi = upkr_state >> 8; 151 | let lazy bit = state_low < prob; 152 | 153 | upkr_state = state_low + select(bit, prob * state_hi, (0x100 - prob) * state_hi - prob); 154 | 155 | context_index?0x3c000 = prob + ((7 + bit * 257 - prob) >> 4); 156 | 157 | bit 158 | } 159 | 160 | start fn unpack_base() { 161 | base_end = uncompress(0, 0x3c200); 162 | } 163 | 164 | data 0 { 165 | file("../target/base.upk") 166 | } -------------------------------------------------------------------------------- /platform/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::io::prelude::*; 2 | use std::{fs::File, path::Path}; 3 | 4 | use anyhow::Result; 5 | 6 | fn main() -> Result<()> { 7 | println!("Generating compressed base module"); 8 | uw8_tool::BaseModule::create_binary(&Path::new("target/base.upk"))?; 9 | 10 | println!("Converting font"); 11 | convert_font()?; 12 | 13 | println!("Compiling loader module"); 14 | let loader = curlywas::compile_file("src/loader.cwa", curlywas::Options::default()).0?; 15 | File::create("bin/loader.wasm")?.write_all(&loader)?; 16 | 17 | println!("Loader (including base module): {} bytes", loader.len()); 18 | 19 | println!("Compiling platform module"); 20 | let platform = curlywas::compile_file("src/platform.cwa", curlywas::Options::default()).0?; 21 | println!("Compressing platform module"); 22 | let platform = uw8_tool::pack( 23 | &platform, 24 | &uw8_tool::PackConfig::default().with_compression_level(4), 25 | )?; 26 | File::create("bin/platform.uw8")?.write_all(&platform)?; 27 | println!("Platform module: {} bytes", platform.len()); 28 | 29 | Ok(()) 30 | } 31 | 32 | fn convert_font() -> Result<()> { 33 | let image = lodepng::decode32_file("src/font.png")?; 34 | 35 | assert!(image.width == 128 && image.height == 128); 36 | 37 | let mut font = vec![]; 38 | for char in 0..256 { 39 | for y in 0..8 { 40 | let mut byte = 0u8; 41 | let base = (char % 16 * 8) + (char / 16 * 8 + y) * 128; 42 | for x in 0..8 { 43 | byte += byte; 44 | if image.buffer[base + x].r > 128 { 45 | byte |= 1; 46 | } 47 | } 48 | font.push(byte); 49 | } 50 | } 51 | 52 | File::create("target/font.bin")?.write_all(&font)?; 53 | 54 | Ok(()) 55 | } -------------------------------------------------------------------------------- /release/.gitignore: -------------------------------------------------------------------------------- 1 | /binaries 2 | /build -------------------------------------------------------------------------------- /release/make-release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cd $(dirname $0) 6 | 7 | # build web runtime 8 | pushd ../web 9 | rm -rf .parcel-cache 10 | yarn parcel build src/index.html 11 | popd 12 | 13 | rm -rf build 14 | mkdir -p build/microw8-linux 15 | mkdir build/microw8-macos 16 | mkdir build/microw8-windows 17 | 18 | # unzip binaries build by github actions 19 | pushd binaries 20 | unzip -o uw8-linux.zip 21 | chmod +x uw8 22 | mv uw8 ../build/microw8-linux 23 | unzip -o uw8-macos.zip 24 | chmod +x uw8 25 | mv uw8 ../build/microw8-macos 26 | unzip -o uw8-windows.zip 27 | mv uw8.exe ../build/microw8-windows 28 | popd 29 | 30 | for dir in build/*; do 31 | mkdir $dir/examples 32 | for example in ../examples/curlywas/*.cwa; do 33 | cp $example $dir/examples 34 | done 35 | for example in ../examples/wat/*.wat; do 36 | cp $example $dir/examples 37 | done 38 | 39 | cp -r ../examples/include $dir/include 40 | 41 | mkdir $dir/carts 42 | for example in $dir/examples/*; do 43 | build/microw8-linux/uw8 pack -l 9 $example $dir/carts/$(basename ${example%.*}).uw8 44 | done 45 | 46 | cp ../web/dist/index.html $dir/microw8.html 47 | cp ../README.md $dir 48 | cp ../UNLICENSE $dir 49 | done 50 | 51 | VERSION=$(build/microw8-linux/uw8 version) 52 | 53 | cd build 54 | tar czf microw8-$VERSION-linux.tgz microw8-linux 55 | tar czf microw8-$VERSION-macos.tgz microw8-macos 56 | zip -r -9 microw8-$VERSION-windows.zip microw8-windows -------------------------------------------------------------------------------- /site/.SRCINFO: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exoticorn/microw8/b9b2c2c927eabd54666079266d2abaf74218b946/site/.SRCINFO -------------------------------------------------------------------------------- /site/.gitignore: -------------------------------------------------------------------------------- 1 | /public/ 2 | -------------------------------------------------------------------------------- /site/config.toml: -------------------------------------------------------------------------------- 1 | # The URL the site will be built for 2 | base_url = "https://exoticorn.github.io/microw8" 3 | 4 | # Whether to automatically compile all Sass files in the sass directory 5 | compile_sass = true 6 | 7 | # Whether to build a search index to be used later on by a JavaScript library 8 | build_search_index = false 9 | 10 | theme = "juice" 11 | 12 | [markdown] 13 | # Whether to do syntax highlighting 14 | # Theme can be customised by setting the `highlight_theme` variable to a theme supported by Zola 15 | highlight_code = true 16 | highlight_theme = "ascetic-white" 17 | 18 | [extra] 19 | # Put all your custom variables here 20 | juice_logo_name = "MicroW8" 21 | juice_logo_path = "img/microw8.svg" 22 | juice_extra_menu = [ 23 | { title = "Github", link = "https://github.com/exoticorn/microw8" } 24 | ] -------------------------------------------------------------------------------- /site/content/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | +++ 3 | 4 | ## About 5 | 6 | MicroW8 is a WebAssembly based fantasy console inspired by the likes of [TIC-80](https://tic80.com/), [WASM-4](https://wasm4.org/) and [PICO-8](https://www.lexaloffle.com/pico-8.php). 7 | 8 | The initial motivation behind MicroW8 was to explore whether there was a way to make WebAssembly viable for size-coding. (Size coding being the art of creating tiny (often <= 256 bytes) graphical effects and games.) The available examples so far are all in this space, however, I very carefully made sure that all design decisions make sense from the point of view of bigger projects as well. 9 | 10 | ## Specs 11 | 12 | * Screen: 320x240, 256 colors, 60Hz 13 | * Modules: Up to 256KB (WASM) 14 | * Memory: 256KB 15 | * Gamepad input (D-Pad + 4 Buttons) 16 | 17 | For detailed [documentation see here](docs). 18 | 19 | ## Examples 20 | * [Skip Ahead](v0.2.0#AgVfq24KI2Ok2o8qVtPYj27fSuGnfeSKgbOkIOsaEQMov8TDYQ6UjdjwkZrYcM1i9alo4/+Bhm1PRFEa0YHJlJAk/PGoc2K41rejv9ZSqJqIHNjr7cappqhOR2jT+jk+0b0+U6hO+geRCTP2aufWs7L+f/Z27NFY8LKlqPSv+C6Rd6+ohoKi6sYl5Kcrlf1cyTinV7jTTnmbcXWVDBA5rRKxAGMUTDS8rHxqSztRITOaQVP1pSdYgi/BDdOJOxSOIkeaId84S+Ycls5na7EgwSfVIpgqF+tcfkUecb8t2mQrXA7pyKrh/wzHn5N6Oe5aOgmzY2YpTIct) (249 bytes): A port of my [TIC-80 256byte game](http://tic80.com/play?cart=1735) from LoveByte'21, now with sound 21 | * [Fireworks](v0.2.0#AgwvgP+M59snqjl4CMKw5sqm1Zw9yJCbSviMjeLUdHus2a3yl/a99+uiBeqZgP/2jqSjrLjRk73COMM6OSLpsxK8ugT1kuk/q4hQUqqPpGozHoa0laulzGGcahzdfdJsYaK1sIdeIYS9M5PnJx/Wk9H+PvWEPy2Zvv7I6IW7Fg==) (127 bytes): Some fireworks to welcome 2022. 22 | * [OhNoAnotherTunnel](v0.2.0#AgPP1oEFvPzY/rBZwTumtYn37zeMFgpir1Bkn91jsNcp26VzoUpkAOOJTtnzVBfW+/dGnnIdbq/irBUJztY5wuua80DORTYZndgdwZHcSk15ajc4nyO0g1A6kGWyW56oZk0iPYJA9WtUmoj0Plvy1CGwIZrMe57X7QZcdqc3u6zjTA41Tpiqi9vnO3xbhi8o594Vx0XPXwVzpYq1ZCTYenfAGaXKkDmAFJqiVIsiCg==) (175 bytes): A port of my [entry](http://tic80.com/play?cart=1871) in the Outline'21 bytebattle final 23 | * [Technotunnel](v0.2.0#AhPXpq894LaUhp5+HQf39f39/Jc8g5zUrBSc0uyKh36ivskczhY84h55zL8gWpkdvKuRQI+KIt80isKzh8jkM8nILcx0RUvyk8yjE8TgNsgkcORVI0RY5k3qE4ySjaycxa2DVZH61UWZuLsCouuwT7I80TbmmetQSbMywJ/avrrCZIAH0UzQfvOiCJNG48NI0FFY1vjB7a7dcp8Uqg==) (157 bytes): A port of my [entry](https://tic80.com/play?cart=1873) in the Outline'21 bytebattle quater final 24 | * [Font & Palette](v0.2.0#At/p39+IBnj6ry1TRe7jzVy2A4tXgBvmoW2itzoyF2aM28pGy5QDiKxqrk8l9sbWZLtnAb+jgOfU+9QhpuyCAkhN6gPOU481IUL/df96vNe3h288Dqwhd3sfFpothIVFsMwRK72kW2hiR7zWsaXyy5pNmjR6BJk4piWx9ApT1ZwoUajhk6/zij6itq/FD1U3jj/J3MOwqZ2ef8Bv6ZPQlJIYVf62icGa69wS6SI1qBpIFiF14F8PcztRVbKIxLpT4ArCS6nz6FPnyUkqATGSBNPJ): Just a simple viewer for the default font and palette. 25 | 26 | Examplers for older versions: 27 | 28 | * [Technotunnel B/W](v0.1pre2#AQrDAQHAAQIBfwp9A0AgAUEAsiABQcACb7JDmhkgQ5MiBCAEIASUIAFBwAJtQfgAa7IiBSAFlJKRIgaVIgcgByAAskHQD7KVIgIQAEPNzEw/lCIDlCAHIAeUIAOUIAOUQQGykiADIAOUk5GSIgiUIAOTQQqylCACkiIJqCAFIAaVIAiUQQqylCACkiIKqHMgCEEyspQgBpUiCyACkkEUspSocUEFcbJBArIgC5OUQRaylJeoOgB4IAFBAWoiAUGA2ARIDQALCw==) (199 bytes uncompressed): A port of my [entry](https://tic80.com/play?cart=1873) in the Outline'21 bytebattle quater final (older MicroW8 version with monochrome palette) 29 | * [XorScroll](v0.1pre2#AQovAS0BAX8DQCABIAFBwAJvIABBCm1qIAFBwAJtczoAeCABQQFqIgFBgNgESA0ACws=) (50 bytes uncompressed): A simple scrolling XOR pattern. Fun fact: This is the pre-loaded effect when entering a bytebattle. 30 | * [CircleWorm](v0.1pre2#AQp7AXkCAX8CfUEgEA0DQCABskEEspUiAkECspUgALJBiCeylSIDQQWylJIQAEEBspJBoAGylCACQQOylSADQQSylJIQAEEBspJB+ACylCADQRGylCACQQKylJIQAEECspJBELKUIAFBAmxBP2oQEiABQQFqIgFBP0gNAAsL) (126 bytes uncompressed): Just a test for the circle fill function. 31 | 32 | ## Versions 33 | 34 | ### v0.4.1 35 | 36 | * [Web runtime](../v0.4.1) 37 | * [Linux](https://github.com/exoticorn/microw8/releases/download/v0.4.1/microw8-0.4.1-linux.tgz) 38 | * [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.4.1/microw8-0.4.1-macos.tgz) 39 | * [Windows](https://github.com/exoticorn/microw8/releases/download/v0.4.1/microw8-0.4.1-windows.zip) 40 | 41 | Changes: 42 | 43 | * Windows: fix bad/inconsistent frame rate 44 | * fix choppy sound on devices with sample rates != 44100 kHz 45 | * add scale mode 'fill' option 46 | 47 | 48 | ### Older versions 49 | 50 | [Find older versions here.](versions) 51 | -------------------------------------------------------------------------------- /site/content/versions.md: -------------------------------------------------------------------------------- 1 | +++ 2 | description = "Versions" 3 | +++ 4 | 5 | ### v0.4.1 6 | 7 | * [Web runtime](../v0.4.1) 8 | * [Linux](https://github.com/exoticorn/microw8/releases/download/v0.4.1/microw8-0.4.1-linux.tgz) 9 | * [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.4.1/microw8-0.4.1-macos.tgz) 10 | * [Windows](https://github.com/exoticorn/microw8/releases/download/v0.4.1/microw8-0.4.1-windows.zip) 11 | 12 | Changes: 13 | 14 | * Windows: fix bad/inconsistent frame rate 15 | * fix choppy sound on devices with sample rates != 44100 kHz 16 | * add scale mode 'fill' option 17 | 18 | ### v0.4.0 19 | 20 | * [Web runtime](../v0.4.0) 21 | * [Linux](https://github.com/exoticorn/microw8/releases/download/v0.4.0/microw8-0.4.0-linux.tgz) 22 | * [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.4.0/microw8-0.4.0-macos.tgz) 23 | * [Windows](https://github.com/exoticorn/microw8/releases/download/v0.4.0/microw8-0.4.0-windows.zip) 24 | 25 | Changes: 26 | 27 | * add support for sound on mono- and surround-only devices 28 | * update wasmtime dependency to fix performance regression in 0.3.0 29 | * add frame counter since module start at location 72 30 | * add 6 and 7 parameter function types to base module 31 | 32 | ### v0.3.0 33 | 34 | * [Web runtime](../v0.3.0) 35 | * [Linux](https://github.com/exoticorn/microw8/releases/download/v0.3.0/microw8-0.3.0-linux.tgz) 36 | * [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.3.0/microw8-0.3.0-macos.tgz) 37 | * [Windows](https://github.com/exoticorn/microw8/releases/download/v0.3.0/microw8-0.3.0-windows.zip) 38 | 39 | Changes: 40 | 41 | * add blitSprite and grabSprite API calls 42 | * add support for integer scaling up to 16x for printing text 43 | * fix incompatibility with sound devices only offering 16bit audio formats 44 | * add support for br_table instruction in packed carts 45 | 46 | ### v0.2.2 47 | 48 | * [Web runtime](../v0.2.2) 49 | * [Linux](https://github.com/exoticorn/microw8/releases/download/v0.2.2/microw8-0.2.2-linux.tgz) 50 | * [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.2.2/microw8-0.2.2-macos.tgz) 51 | * [Windows](https://github.com/exoticorn/microw8/releases/download/v0.2.2/microw8-0.2.2-windows.zip) 52 | 53 | Changes: 54 | 55 | * call `start` function after loading cart if the cart exports one 56 | * fix `sndGes` having the wrong name and not being included in the auto imports 57 | * fix control codes 4-6 (change text output mode) being invoked when used as parameters in other control sequences 58 | * only open browser window once a cart was compiled sucessfully when running with `-b` 59 | 60 | ### v0.2.1 61 | 62 | * [Web runtime](../v0.2.1) 63 | * [Linux](https://github.com/exoticorn/microw8/releases/download/v0.2.1/microw8-0.2.1-linux.tgz) 64 | * [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.2.1/microw8-0.2.1-macos.tgz) 65 | * [Windows](https://github.com/exoticorn/microw8/releases/download/v0.2.1/microw8-0.2.1-windows.zip) 66 | 67 | Changes: 68 | 69 | * new gpu accelerated renderer with (optional) crt filter 70 | * optimized `hline` function, a big speed-up when drawing large filled circles or rectangles 71 | * print fractional size of packed `uw8` cart 72 | 73 | ### v0.2.0 74 | 75 | * [Web runtime](../v0.2.0) 76 | * [Linux](https://github.com/exoticorn/microw8/releases/download/v0.2.0/microw8-0.2.0-linux.tgz) 77 | * [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.2.0/microw8-0.2.0-macos.tgz) 78 | * [Windows](https://github.com/exoticorn/microw8/releases/download/v0.2.0/microw8-0.2.0-windows.zip) 79 | 80 | Changes: 81 | 82 | * [add sound support!](docs#sound) 83 | * add support to redirect text output to the console for debugging using control code 6 84 | * update curlywas: 85 | * add support for `else if` 86 | * add support for escape sequences in strings 87 | * add support for char literals 88 | * add support for binop-assignment, eg. `+=`, `^=`, `<<=` etc. (also support for the tee operator: `+:=`) 89 | * "integer constant cast to float" literal syntax in CurlyWas (ex. `1_f` is equivalent to `1 as f32`) 90 | 91 | ### v0.2.0-rc3 92 | 93 | * [Web runtime](../v0.2.0-rc3) 94 | * [Linux](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc3/microw8-0.2.0-rc3-linux.tgz) 95 | * [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc3/microw8-0.2.0-rc3-macos.tgz) 96 | * [Windows](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc3/microw8-0.2.0-rc3-windows.zip) 97 | 98 | Changes: 99 | 100 | * improve timing stability some more. essentially now guaranteeing that "frame = time_ms * 6 / 100" returns 101 | consecutive frame numbers, provided the module can be run at 60 fps 102 | * add support to redirect text output to the console for debugging using control code 6 103 | * update curlywas: 104 | * add support for `else if` 105 | * add support for escape sequences in strings 106 | * add support for char literals 107 | * add support for binop-assignment, eg. `+=`, `^=`, `<<=` etc. (also support for the tee operator: `+:=`) 108 | 109 | ### v0.2.0-rc2 110 | 111 | * [Web runtime](../v0.2.0-rc2) 112 | * [Linux](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc2/microw8-0.2.0-rc2-linux.tgz) 113 | * [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc2/microw8-0.2.0-rc2-macos.tgz) 114 | * [Windows](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc2/microw8-0.2.0-rc2-windows.zip) 115 | 116 | Changes: 117 | 118 | * fix timing issues of sound playback, especially on systems with large sound buffers 119 | 120 | ### v0.2.0-rc1 121 | 122 | * [Web runtime](../v0.2.0-rc1) 123 | * [Linux](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc1/microw8-0.2.0-rc1-linux.tgz) 124 | * [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc1/microw8-0.2.0-rc1-macos.tgz) 125 | * [Windows](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc1/microw8-0.2.0-rc1-windows.zip) 126 | 127 | Changes: 128 | 129 | * [add sound support](docs#sound) 130 | * "integer constant cast to float" literal syntax in CurlyWas (ex. `1_f` is equivalent to `1 as f32`) 131 | 132 | Known issues: 133 | 134 | * timing accuracy/update frequency of sound support currently depends on sound buffer size 135 | 136 | ### v0.1.2 137 | 138 | * [Web runtime](../v0.1.2) 139 | * [Linux](https://github.com/exoticorn/microw8/releases/download/v0.1.2/microw8-0.1.2-linux.tgz) 140 | * [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.1.2/microw8-0.1.2-macos.tgz) 141 | * [Windows](https://github.com/exoticorn/microw8/releases/download/v0.1.2/microw8-0.1.2-windows.zip) 142 | 143 | Changes: 144 | 145 | * add option to `uw8 run` to run the cart in the browser using the web runtime 146 | *../ CurlyWas: implement `include` support 147 | * CurlyWas: implement support for constants 148 | * fix crash when trying to draw zero sized line 149 | 150 | ### v0.1.1 151 | 152 | * [Web runtime](../v0.1.1) 153 | * [Linux](https://github.com/exoticorn/microw8/releases/download/v0.1.1/microw8-0.1.1-linux.tgz) 154 | * [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.1.1/microw8-0.1.1-macos.tgz) 155 | * [Windows](https://github.com/exoticorn/microw8/releases/download/v0.1.1/microw8-0.1.1-windows.zip) 156 | 157 | Changes: 158 | 159 | * implement more robust file watcher 160 | * add basic video recording on F10 in web runtime 161 | *../ add screenshot on F9 162 | * add watchdog to interrupt hanging update in native runtime 163 | * add devkit mode to web runtime 164 | *../ add unpack and compile commands to uw8 165 | * add support for table/element section in pack command 166 | * disable wayland support (caused missing window decorations in gnome) 167 | 168 | ### v0.1.0 169 | 170 | * [Web runtime](../v0.1.0) 171 | * [Linux](https://github.com/exoticorn/microw8/releases/download/v0.1.0/microw8-0.1.0-linux.tgz) 172 | * [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.1.0/microw8-0.1.0-macos.tgz) 173 | * [Windows](https://github.com/exoticorn/microw8/releases/download/v0.1.0/microw8-0.1.0-windows.zip) 174 | -------------------------------------------------------------------------------- /site/static/img/microw8.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /site/static/img/technotunnel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exoticorn/microw8/b9b2c2c927eabd54666079266d2abaf74218b946/site/static/img/technotunnel.png -------------------------------------------------------------------------------- /site/static/uw8/skipahead.uw8: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exoticorn/microw8/b9b2c2c927eabd54666079266d2abaf74218b946/site/static/uw8/skipahead.uw8 -------------------------------------------------------------------------------- /site/static/v0.1.0/index.html: -------------------------------------------------------------------------------- 1 | MicroW8
MicroW8 0.1.0
-------------------------------------------------------------------------------- /site/static/v0.1pre1/index.html: -------------------------------------------------------------------------------- 1 | MicroW8
-------------------------------------------------------------------------------- /site/static/v0.1pre2/index.html: -------------------------------------------------------------------------------- 1 | MicroW8
-------------------------------------------------------------------------------- /site/static/v0.1pre3/index.html: -------------------------------------------------------------------------------- 1 | MicroW8
-------------------------------------------------------------------------------- /site/static/v0.1pre4/index.html: -------------------------------------------------------------------------------- 1 | MicroW8
-------------------------------------------------------------------------------- /site/static/v0.1pre5/index.html: -------------------------------------------------------------------------------- 1 | MicroW8
-------------------------------------------------------------------------------- /site/templates/_variables.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /site/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "juice/templates/index.html" %} 2 | {% block hero %} 3 |
4 |
5 |

A WebAssembly based fantasy console

6 |
7 | 8 | 9 | 10 |
11 |
13 | Explore More ⇩ 14 |
15 | {% endblock hero %} 16 | 17 | {% block footer %} 18 | {% endblock footer %} 19 | -------------------------------------------------------------------------------- /src/filewatcher.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, bail, Result}; 2 | use notify_debouncer_mini::{ 3 | new_debouncer, 4 | notify::{self, RecommendedWatcher}, 5 | DebouncedEvent, DebouncedEventKind, Debouncer, 6 | }; 7 | use std::{collections::BTreeSet, path::PathBuf, sync::mpsc, time::Duration}; 8 | 9 | pub struct FileWatcher { 10 | debouncer: Debouncer, 11 | watched_files: BTreeSet, 12 | directories: BTreeSet, 13 | rx: mpsc::Receiver, 14 | } 15 | 16 | impl FileWatcher { 17 | pub fn new() -> Result { 18 | let (tx, rx) = mpsc::channel(); 19 | let debouncer = new_debouncer(Duration::from_millis(100), move |res| match res { 20 | Ok(events) => { 21 | for event in events { 22 | let _ = tx.send(event); 23 | } 24 | } 25 | Err(err) => { 26 | eprintln!("Error watching for file changes: {err}"); 27 | } 28 | })?; 29 | Ok(FileWatcher { 30 | debouncer, 31 | watched_files: BTreeSet::new(), 32 | directories: BTreeSet::new(), 33 | rx, 34 | }) 35 | } 36 | 37 | pub fn add_file>(&mut self, path: P) -> Result<()> { 38 | let path = path.into(); 39 | let parent = path.parent().ok_or_else(|| anyhow!("File has no parent"))?; 40 | 41 | if !self.directories.contains(parent) { 42 | self.debouncer 43 | .watcher() 44 | .watch(parent, notify::RecursiveMode::NonRecursive)?; 45 | self.directories.insert(parent.to_path_buf()); 46 | } 47 | 48 | self.watched_files.insert(path); 49 | Ok(()) 50 | } 51 | 52 | pub fn poll_changed_file(&self) -> Result> { 53 | match self.rx.try_recv() { 54 | Ok(event) => match event.kind { 55 | DebouncedEventKind::Any => { 56 | let handle = same_file::Handle::from_path(&event.path)?; 57 | for file in &self.watched_files { 58 | if handle == same_file::Handle::from_path(file)? { 59 | return Ok(Some(event.path)); 60 | } 61 | } 62 | } 63 | _ => (), 64 | }, 65 | Err(mpsc::TryRecvError::Disconnected) => bail!("File watcher disconnected"), 66 | _ => (), 67 | } 68 | 69 | Ok(None) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod filewatcher; 2 | #[cfg(feature = "native")] 3 | mod run_native; 4 | #[cfg(feature = "browser")] 5 | mod run_web; 6 | 7 | pub use filewatcher::FileWatcher; 8 | #[cfg(feature = "native")] 9 | pub use run_native::MicroW8; 10 | #[cfg(feature = "browser")] 11 | pub use run_web::RunWebServer; 12 | 13 | use anyhow::Result; 14 | 15 | pub trait Runtime { 16 | fn is_open(&self) -> bool; 17 | fn load(&mut self, module_data: &[u8]) -> Result<()>; 18 | fn run_frame(&mut self) -> Result<()>; 19 | } 20 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::prelude::*; 3 | use std::path::{Path, PathBuf}; 4 | use std::process; 5 | 6 | use anyhow::Result; 7 | use pico_args::Arguments; 8 | #[cfg(feature = "native")] 9 | use uw8::MicroW8; 10 | #[cfg(feature = "browser")] 11 | use uw8::RunWebServer; 12 | #[cfg(any(feature = "native", feature = "browser"))] 13 | use uw8::Runtime; 14 | 15 | fn main() -> Result<()> { 16 | env_logger::Builder::from_env(env_logger::Env::default()).init(); 17 | let mut args = Arguments::from_env(); 18 | 19 | // try to enable ansi support in win10 cmd shell 20 | #[cfg(target_os = "windows")] 21 | let _ = ansi_term::enable_ansi_support(); 22 | 23 | match args.subcommand()?.as_deref() { 24 | Some("version") => { 25 | println!("{}", env!("CARGO_PKG_VERSION")); 26 | Ok(()) 27 | } 28 | #[cfg(any(feature = "native", feature = "browser"))] 29 | Some("run") => run(args), 30 | Some("pack") => pack(args), 31 | Some("unpack") => unpack(args), 32 | Some("compile") => compile(args), 33 | Some("filter-exports") => filter_exports(args), 34 | Some("help") | None => { 35 | println!("uw8 {}", env!("CARGO_PKG_VERSION")); 36 | println!(); 37 | println!("Usage:"); 38 | #[cfg(any(feature = "native", feature = "browser"))] 39 | println!(" uw8 run [-t/--timeout ] [--b/--browser] [-w/--watch] [-p/--pack] [-u/--uncompressed] [-l/--level] [-o/--output ] "); 40 | println!(" uw8 pack [-u/--uncompressed] [-l/--level] "); 41 | println!(" uw8 unpack "); 42 | println!(" uw8 compile [-d/--debug] "); 43 | println!(" uw8 filter-exports "); 44 | Ok(()) 45 | } 46 | Some(other) => { 47 | eprintln!("Unknown command '{}'", other); 48 | process::exit(1); 49 | } 50 | } 51 | } 52 | 53 | #[cfg(any(feature = "native", feature = "browser"))] 54 | fn run(mut args: Arguments) -> Result<()> { 55 | let watch_mode = args.contains(["-w", "--watch"]); 56 | #[allow(unused)] 57 | let timeout: Option = args.opt_value_from_str(["-t", "--timeout"])?; 58 | 59 | let mut config = Config::default(); 60 | if args.contains(["-p", "--pack"]) { 61 | let mut pack = uw8_tool::PackConfig::default(); 62 | if args.contains(["-u", "--uncompressed"]) { 63 | pack = pack.uncompressed(); 64 | } 65 | 66 | if let Some(level) = args.opt_value_from_str(["-l", "--level"])? { 67 | pack = pack.with_compression_level(level); 68 | } 69 | 70 | config.pack = Some(pack); 71 | } 72 | 73 | if let Some(path) = 74 | args.opt_value_from_os_str::<_, _, bool>(["-o", "--output"], |s| Ok(s.into()))? 75 | { 76 | config.output_path = Some(path); 77 | } 78 | 79 | #[cfg(feature = "native")] 80 | let run_browser = args.contains(["-b", "--browser"]); 81 | #[cfg(not(feature = "native"))] 82 | let run_browser = args.contains(["-b", "--browser"]) || true; 83 | 84 | #[allow(unused)] 85 | let disable_audio = args.contains(["-m", "--no-audio"]); 86 | 87 | #[cfg(feature = "native")] 88 | let window_config = { 89 | let mut config = uw8_window::WindowConfig::default(); 90 | if !run_browser { 91 | config.parse_arguments(&mut args); 92 | } 93 | config 94 | }; 95 | 96 | let filename = args.free_from_os_str::(|s| Ok(s.into()))?; 97 | 98 | let mut watcher = uw8::FileWatcher::new()?; 99 | 100 | use std::process::exit; 101 | 102 | let mut runtime: Box = if !run_browser { 103 | #[cfg(not(feature = "native"))] 104 | unimplemented!(); 105 | #[cfg(feature = "native")] 106 | { 107 | let mut microw8 = MicroW8::new(timeout, window_config)?; 108 | if disable_audio { 109 | microw8.disable_audio(); 110 | } 111 | Box::new(microw8) 112 | } 113 | } else { 114 | #[cfg(not(feature = "browser"))] 115 | unimplemented!(); 116 | #[cfg(feature = "browser")] 117 | Box::new(RunWebServer::new()) 118 | }; 119 | 120 | let mut first_run = true; 121 | 122 | while runtime.is_open() { 123 | if first_run || watcher.poll_changed_file()?.is_some() { 124 | let (result, dependencies) = start_cart(&filename, &mut *runtime, &config); 125 | if watch_mode { 126 | for dep in dependencies { 127 | watcher.add_file(dep)?; 128 | } 129 | } 130 | if let Err(err) = result { 131 | eprintln!("Load error: {}", err); 132 | if !watch_mode { 133 | exit(1); 134 | } 135 | } 136 | first_run = false; 137 | } 138 | 139 | if let Err(err) = runtime.run_frame() { 140 | eprintln!("Runtime error: {}", err); 141 | if !watch_mode { 142 | exit(1); 143 | } 144 | } 145 | } 146 | 147 | Ok(()) 148 | } 149 | 150 | #[derive(Default)] 151 | struct Config { 152 | pack: Option, 153 | output_path: Option, 154 | } 155 | 156 | fn load_cart(filename: &Path, config: &Config) -> (Result>, Vec) { 157 | let mut dependencies = Vec::new(); 158 | fn inner(filename: &Path, config: &Config, dependencies: &mut Vec) -> Result> { 159 | let mut cart = match SourceType::of_file(filename)? { 160 | SourceType::Binary => { 161 | let mut cart = vec![]; 162 | File::open(filename)?.read_to_end(&mut cart)?; 163 | cart 164 | } 165 | SourceType::Wat => { 166 | let cart = wat::parse_file(filename)?; 167 | cart 168 | } 169 | SourceType::CurlyWas => { 170 | let (module, deps) = curlywas::compile_file(filename, curlywas::Options::default()); 171 | *dependencies = deps; 172 | module? 173 | } 174 | }; 175 | 176 | if let Some(ref pack_config) = config.pack { 177 | cart = uw8_tool::pack(&cart, pack_config)?; 178 | println!( 179 | "\npacked size: {} bytes ({:.2})", 180 | cart.len(), 181 | uw8_tool::compressed_size(&cart) 182 | ); 183 | } 184 | 185 | if let Some(ref path) = config.output_path { 186 | File::create(path)?.write_all(&cart)?; 187 | } 188 | 189 | Ok(cart) 190 | } 191 | 192 | let result = inner(filename, config, &mut dependencies); 193 | 194 | if dependencies.is_empty() { 195 | dependencies.push(filename.to_path_buf()); 196 | } 197 | 198 | (result, dependencies) 199 | } 200 | 201 | enum SourceType { 202 | Binary, 203 | Wat, 204 | CurlyWas, 205 | } 206 | 207 | impl SourceType { 208 | fn of_file(filename: &Path) -> Result { 209 | if let Some(extension) = filename.extension() { 210 | if extension == "uw8" || extension == "wasm" { 211 | return Ok(SourceType::Binary); 212 | } else if extension == "wat" || extension == "wast" { 213 | return Ok(SourceType::Wat); 214 | } else if extension == "cwa" { 215 | return Ok(SourceType::CurlyWas); 216 | } 217 | } 218 | 219 | let mut cart = vec![]; 220 | File::open(filename)?.read_to_end(&mut cart)?; 221 | 222 | let ty = if cart[0] < 10 { 223 | SourceType::Binary 224 | } else { 225 | let src = String::from_utf8(cart)?; 226 | if src.chars().find(|&c| !c.is_whitespace() && c != ';') == Some('(') { 227 | SourceType::Wat 228 | } else { 229 | SourceType::CurlyWas 230 | } 231 | }; 232 | Ok(ty) 233 | } 234 | } 235 | 236 | #[cfg(any(feature = "native", feature = "browser"))] 237 | fn start_cart( 238 | filename: &Path, 239 | runtime: &mut dyn Runtime, 240 | config: &Config, 241 | ) -> (Result<()>, Vec) { 242 | let (cart, dependencies) = load_cart(filename, config); 243 | let cart = match cart { 244 | Ok(cart) => cart, 245 | Err(err) => return (Err(err), dependencies), 246 | }; 247 | 248 | if let Err(err) = runtime.load(&cart) { 249 | eprintln!("Load error: {}", err); 250 | (Err(err), dependencies) 251 | } else { 252 | (Ok(()), dependencies) 253 | } 254 | } 255 | 256 | fn pack(mut args: Arguments) -> Result<()> { 257 | let mut pack_config = uw8_tool::PackConfig::default(); 258 | 259 | if args.contains(["-u", "--uncompressed"]) { 260 | pack_config = pack_config.uncompressed(); 261 | } 262 | 263 | if let Some(level) = args.opt_value_from_str(["-l", "--level"])? { 264 | pack_config = pack_config.with_compression_level(level); 265 | } 266 | 267 | let in_file = args.free_from_os_str::(|s| Ok(s.into()))?; 268 | 269 | let out_file = args.free_from_os_str::(|s| Ok(s.into()))?; 270 | 271 | let cart = load_cart( 272 | &in_file, 273 | &Config { 274 | pack: Some(pack_config), 275 | output_path: None, 276 | }, 277 | ) 278 | .0?; 279 | 280 | File::create(out_file)?.write_all(&cart)?; 281 | 282 | Ok(()) 283 | } 284 | 285 | fn unpack(mut args: Arguments) -> Result<()> { 286 | let in_file = args.free_from_os_str::(|s| Ok(s.into()))?; 287 | let out_file = args.free_from_os_str::(|s| Ok(s.into()))?; 288 | 289 | uw8_tool::unpack_file(&in_file, &out_file) 290 | } 291 | 292 | fn compile(mut args: Arguments) -> Result<()> { 293 | let mut options = curlywas::Options::default(); 294 | if args.contains(["-d", "--debug"]) { 295 | options = options.with_debug(); 296 | } 297 | 298 | let in_file = args.free_from_os_str::(|s| Ok(s.into()))?; 299 | let out_file = args.free_from_os_str::(|s| Ok(s.into()))?; 300 | 301 | let module = curlywas::compile_file(in_file, options).0?; 302 | File::create(out_file)?.write_all(&module)?; 303 | 304 | Ok(()) 305 | } 306 | 307 | fn filter_exports(mut args: Arguments) -> Result<()> { 308 | let in_file = args.free_from_os_str::(|s| Ok(s.into()))?; 309 | let out_file = args.free_from_os_str::(|s| Ok(s.into()))?; 310 | 311 | uw8_tool::filter_exports(&in_file, &out_file)?; 312 | 313 | Ok(()) 314 | } 315 | -------------------------------------------------------------------------------- /src/run_web.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::{ 3 | net::SocketAddr, 4 | sync::{Arc, Mutex}, 5 | thread, 6 | }; 7 | use tokio::sync::broadcast; 8 | use tokio_stream::{wrappers::BroadcastStream, Stream, StreamExt}; 9 | use warp::{http::Response, Filter}; 10 | 11 | pub struct RunWebServer { 12 | cart: Arc>>, 13 | tx: broadcast::Sender<()>, 14 | socket_addr: SocketAddr, 15 | } 16 | 17 | impl RunWebServer { 18 | pub fn new() -> RunWebServer { 19 | let cart = Arc::new(Mutex::new(Vec::new())); 20 | let (tx, _) = broadcast::channel(1); 21 | 22 | let socket_addr = "127.0.0.1:3030" 23 | .parse::() 24 | .expect("Failed to parse socket address"); 25 | 26 | let server_cart = cart.clone(); 27 | let server_tx = tx.clone(); 28 | let server_addr = socket_addr.clone(); 29 | thread::spawn(move || { 30 | let rt = tokio::runtime::Builder::new_current_thread() 31 | .enable_io() 32 | .enable_time() 33 | .build() 34 | .expect("Failed to create tokio runtime"); 35 | rt.block_on(async { 36 | let html = warp::path::end().map(|| { 37 | Response::builder() 38 | .header("Content-Type", "text/html") 39 | .body(include_str!("run-web.html")) 40 | }); 41 | 42 | let cart = warp::path("cart") 43 | .map(move || server_cart.lock().map_or(Vec::new(), |c| c.clone())); 44 | 45 | let events = warp::path("events").and(warp::get()).map(move || { 46 | fn event_stream( 47 | tx: &broadcast::Sender<()>, 48 | ) -> impl Stream> 49 | { 50 | BroadcastStream::new(tx.subscribe()) 51 | .map(|_| Ok(warp::sse::Event::default().data("L"))) 52 | } 53 | warp::sse::reply(warp::sse::keep_alive().stream(event_stream(&server_tx))) 54 | }); 55 | 56 | let server_future = warp::serve(html.or(cart).or(events)).bind(server_addr); 57 | server_future.await 58 | }); 59 | }); 60 | 61 | RunWebServer { 62 | cart, 63 | tx, 64 | socket_addr, 65 | } 66 | } 67 | } 68 | 69 | impl super::Runtime for RunWebServer { 70 | fn load(&mut self, module_data: &[u8]) -> Result<()> { 71 | if let Ok(mut lock) = self.cart.lock() { 72 | if lock.is_empty() && !module_data.is_empty() { 73 | println!("Point browser at http://{}", self.socket_addr); 74 | let _ignore_result = webbrowser::open(&format!("http://{}", self.socket_addr)); 75 | } 76 | lock.clear(); 77 | lock.extend_from_slice(module_data); 78 | } 79 | let _ignore_result = self.tx.send(()); 80 | Ok(()) 81 | } 82 | 83 | fn is_open(&self) -> bool { 84 | true 85 | } 86 | 87 | fn run_frame(&mut self) -> Result<()> { 88 | std::thread::sleep(std::time::Duration::from_millis(100)); 89 | Ok(()) 90 | } 91 | } 92 | 93 | impl Default for RunWebServer { 94 | fn default() -> RunWebServer { 95 | RunWebServer::new() 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /syntax/SublimeText/CurlyWASM.sublime-syntax: -------------------------------------------------------------------------------- 1 | %YAML 1.2 2 | --- 3 | # ========================================================== 4 | # http://www.sublimetext.com/docs/syntax.html 5 | # created by: zbyti & various AI 6 | # ========================================================== 7 | # For best results, use with Rust-themed color schemes like: 8 | # - "RustEnhanced" 9 | # - "Atomized" 10 | # - "Solarized Rust" 11 | # ========================================================== 12 | name: CurlyWASM 13 | file_extensions: [cwa] 14 | scope: source.curlywasm 15 | 16 | contexts: 17 | main: 18 | # Comments 19 | - match: /\* 20 | scope: comment.block.curlywasm 21 | push: block_comment 22 | - match: // 23 | scope: comment.line.curlywasm 24 | push: line_comment 25 | 26 | # Module system 27 | - match: \b(import|include|export)\b 28 | scope: keyword.control.import.curlywasm 29 | 30 | # Declarations and definitions 31 | - match: \b(fn)\b 32 | scope: keyword.declaration.function.curlywasm 33 | 34 | - match: \b(const)\b 35 | scope: storage.modifier.const.curlywasm 36 | 37 | - match: \b(global|mut|let|lazy|inline)\b 38 | scope: storage.modifier.curlywasm 39 | 40 | # Control flow 41 | - match: \b(if|else|block|loop|branch|branch_if)\b 42 | scope: keyword.control.flow.curlywasm 43 | 44 | # Type conversion 45 | - match: \b(as)\b 46 | scope: keyword.operator.type.curlywasm 47 | 48 | # WASM memory access operators 49 | - match: \b(load|store)\b 50 | scope: keyword.operator.memory.curlywasm 51 | 52 | # API functions 53 | - match: \b(sin|cos|tan|asin|acos|atan|atan2|pow|log|fmod|random|randomf|randomSeed|cls|setPixel|getPixel|hline|rectangle|circle|line|time|isButtonPressed|isButtonTriggered|printChar|printString|printInt|setTextColor|setBackgroundColor|setCursorPosition|rectangleOutline|circleOutline|exp|playNote|sndGes|blitSprite|grabSprite)\b 54 | scope: support.function.curlywasm 55 | 56 | # Built-in functions 57 | - match: \b(start|upd|sqrt|min|max|ceil|floor|trunc|nearest|abs|copysign|select)\b 58 | scope: support.function.curlywasm 59 | 60 | # Data blocks - Match 'data {' together and push context 61 | - match: \b(data)\s*(\{) 62 | captures: 63 | 1: storage.type.data.curlywasm 64 | 2: punctuation.section.block.begin.curlywasm 65 | push: data_content 66 | 67 | # Base types 68 | - match: \b(i8|i16|i32|i64|f32|f64)\b 69 | scope: storage.type.primitive.curlywasm 70 | 71 | # Memory access operators 72 | - match: (\?|\$|\!) 73 | scope: keyword.operator.memory.curlywasm 74 | 75 | # Operators 76 | - match: (->) 77 | scope: keyword.operator.arrow.curlywasm 78 | 79 | # Assignment operators 80 | - match: (=|:=|\+=|-=|\*=|/=|%=|&=|\|=|\^=|#/=) 81 | scope: keyword.operator.assignment.curlywasm 82 | 83 | # Arithmetic operators 84 | - match: (\+|-|\*|/|%|#/|#%) 85 | scope: keyword.operator.arithmetic.curlywasm 86 | 87 | # Bitwise operators 88 | - match: (\&|\||\^|<<|>>|#>>) 89 | scope: keyword.operator.bitwise.curlywasm 90 | 91 | # Comparison operators 92 | - match: (<|>|<=|>=|#<|#<=|#>|#>=|==|!=) 93 | scope: keyword.operator.comparison.curlywasm 94 | 95 | # Other operators 96 | - match: (<\|) 97 | scope: keyword.operator.misc.curlywasm 98 | 99 | # Numeric literals 100 | - match: \b(0x[0-9a-fA-F]+)\b 101 | scope: constant.numeric.hex.curlywasm 102 | 103 | - match: '\b\d+(_f)\b' 104 | scope: constant.numeric.float.curlywasm 105 | 106 | - match: \b0x[0-9a-fA-F]+_f\b 107 | scope: constant.numeric.float.curlywasm 108 | 109 | - match: \b([0-9]+\.[0-9]+)\b 110 | scope: constant.numeric.float.curlywasm 111 | 112 | - match: \b([0-9]+)\b 113 | scope: constant.numeric.integer.curlywasm 114 | 115 | # String literals 116 | - match: \" 117 | scope: punctuation.definition.string.begin.curlywasm 118 | push: double_quoted_string 119 | - match: \' 120 | scope: punctuation.definition.string.begin.curlywasm 121 | push: single_quoted_string 122 | 123 | # Function calls 124 | - match: \b([a-zA-Z_][a-zA-Z0-9_]*)\s*\( 125 | captures: 126 | 1: entity.name.function.call.curlywasm 127 | 128 | # Function declarations 129 | - match: \bfn\s+([a-zA-Z_][a-zA-Z0-9_]*)\b 130 | captures: 131 | 1: entity.name.function.declaration.curlywasm 132 | 133 | # Constants (Upper case convention) 134 | - match: \b([A-Z_][A-Z0-9_]*)\b 135 | scope: constant.other.curlywasm 136 | 137 | # Variables (Lower case convention) 138 | - match: \b([a-z_][a-zA-Z0-9_]*)\b 139 | scope: variable.other.curlywasm 140 | 141 | # Punctuation 142 | - match: \{ 143 | scope: punctuation.section.block.begin.curlywasm 144 | - match: \} 145 | scope: punctuation.section.block.end.curlywasm 146 | - match: \( 147 | scope: punctuation.section.group.begin.curlywasm 148 | - match: \) 149 | scope: punctuation.section.group.end.curlywasm 150 | - match: \[ 151 | scope: punctuation.section.brackets.begin.curlywasm 152 | - match: \] 153 | scope: punctuation.section.brackets.end.curlywasm 154 | - match: ; 155 | scope: punctuation.terminator.curlywasm 156 | - match: \, 157 | scope: punctuation.separator.curlywasm 158 | - match: ':' 159 | scope: punctuation.separator.type.curlywasm 160 | 161 | # Context for /* ... */ block comments 162 | block_comment: 163 | - meta_scope: comment.block.curlywasm 164 | - match: \*/ 165 | scope: punctuation.definition.comment.end.curlywasm 166 | pop: true 167 | 168 | # Context for // ... line comments 169 | line_comment: 170 | - meta_scope: comment.line.double-slash.curlywasm 171 | - match: $ # Pop at the end of the line 172 | pop: true 173 | 174 | # Context for "..." strings 175 | double_quoted_string: 176 | - meta_scope: string.quoted.double.curlywasm 177 | - match: \" 178 | scope: punctuation.definition.string.end.curlywasm 179 | pop: true 180 | - match: \\. # Escape sequences 181 | scope: constant.character.escape.curlywasm 182 | 183 | # Context for '...' strings 184 | single_quoted_string: 185 | - meta_scope: string.quoted.single.curlywasm 186 | - match: \' 187 | scope: punctuation.definition.string.end.curlywasm 188 | pop: true 189 | - match: \\. # Escape sequences 190 | scope: constant.character.escape.curlywasm 191 | 192 | # Context for the content inside data { ... } 193 | data_content: 194 | - meta_scope: meta.data.content.curlywasm 195 | # Match the closing brace to pop the context 196 | - match: \} 197 | scope: punctuation.section.block.end.curlywasm 198 | pop: true 199 | # Include rules for literals within the data block 200 | - include: literals 201 | # Specific types/keywords allowed inside data blocks 202 | - match: \b(i8|i16|i32|i64|f32|f64)\b 203 | scope: storage.type.primitive.curlywasm 204 | - match: \b(file)\b 205 | scope: keyword.control.curlywasm 206 | # Punctuation inside data blocks 207 | - match: \( 208 | scope: punctuation.section.group.begin.curlywasm 209 | - match: \) 210 | scope: punctuation.section.group.end.curlywasm 211 | - match: \, 212 | scope: punctuation.separator.curlywasm 213 | # Potentially allow comments inside data blocks 214 | - include: block_comment 215 | - include: line_comment 216 | 217 | # Reusable patterns for literals (used via include) 218 | literals: 219 | # Numeric literals 220 | - match: \b(0x[0-9a-fA-F]+)\b 221 | scope: constant.numeric.hex.curlywasm 222 | - match: '\b\d+(_f)\b' 223 | scope: constant.numeric.float.curlywasm 224 | - match: \b([0-9]+\.[0-9]+)\b 225 | scope: constant.numeric.float.curlywasm 226 | - match: \b([0-9]+)\b 227 | scope: constant.numeric.integer.curlywasm 228 | # String literals 229 | - match: \" 230 | scope: punctuation.definition.string.begin.curlywasm 231 | push: double_quoted_string 232 | - match: \' 233 | scope: punctuation.definition.string.begin.curlywasm 234 | push: single_quoted_string 235 | -------------------------------------------------------------------------------- /syntax/SublimeText/data-block.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 9 | datatype 10 | Data block definition 11 | 12 | -------------------------------------------------------------------------------- /syntax/SublimeText/fn-export.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | ${3:return_type} { 4 | ${4:// Function body} 5 | } 6 | ]]> 7 | fnexport 8 | Exported function definition 9 | 10 | -------------------------------------------------------------------------------- /syntax/SublimeText/if-else.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 9 | ifelse 10 | If-else statement 11 | 12 | -------------------------------------------------------------------------------- /syntax/SublimeText/let-inline.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 5 | letinline 6 | Lazy variable declaration 7 | 8 | -------------------------------------------------------------------------------- /syntax/SublimeText/let-lazy.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 5 | letlazy 6 | Lazy variable declaration 7 | 8 | -------------------------------------------------------------------------------- /syntax/SublimeText/loop-branch_if.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 9 | loopbr 10 | Loop with branch_if 11 | 12 | -------------------------------------------------------------------------------- /test/drawing_test.cwa: -------------------------------------------------------------------------------- 1 | include "../examples/include/microw8-api.cwa" 2 | 3 | global mut counter = 0; 4 | 5 | export fn upd() { 6 | cls(0); 7 | 8 | let col: i32 = 1; 9 | 10 | loop colors { 11 | if !testCircle(counter, col) { 12 | printInt(counter); 13 | return; 14 | } 15 | counter += 1; 16 | branch_if (col +:= 1) < 256: colors; 17 | } 18 | } 19 | 20 | fn testCircle(seed: i32, col: i32) -> i32 { 21 | randomSeed(seed); 22 | let cx = randomf() * 640_f - 160_f; 23 | let cy = randomf() * 480_f - 120_f; 24 | let radius = randomf() * 4_f; 25 | radius *= radius; 26 | radius *= radius; 27 | 28 | circle(cx, cy, radius, col); 29 | 30 | let min_x = max(0_f, floor(cx - radius - 1_f)) as i32; 31 | let min_y = max(0_f, floor(cy - radius - 1_f)) as i32; 32 | let max_x = min(320_f, ceil(cx + radius + 1_f)) as i32; 33 | let max_y = min(240_f, ceil(cy + radius + 1_f)) as i32; 34 | 35 | let x = min_x; 36 | loop xloop { 37 | if x < max_x { 38 | let y = min_y; 39 | loop yloop { 40 | if y < max_y { 41 | let rx = x as f32 + 0.5 - cx; 42 | let ry = y as f32 + 0.5 - cy; 43 | let d = sqrt(rx*rx + ry*ry) - radius; 44 | if abs(d) > 0.001 { 45 | let is_inside = d < 0_f; 46 | let is_plotted = getPixel(x, y) == col; 47 | if is_inside != is_plotted { 48 | return 0; 49 | } 50 | } 51 | 52 | y += 1; 53 | branch yloop; 54 | } 55 | } 56 | x += 1; 57 | branch xloop; 58 | } 59 | } 60 | 61 | 1 62 | } -------------------------------------------------------------------------------- /test/frame_time.cwa: -------------------------------------------------------------------------------- 1 | include "../examples/include/microw8-api.cwa" 2 | 3 | global mut pos = 0; 4 | global mut next = 0; 5 | 6 | export fn upd() { 7 | let lazy t = 32!32; 8 | let lazy tick = t * 6 / 100; 9 | let lazy rel = t - tick * 100 / 6; 10 | 11 | setBackgroundColor(select(tick == next, 0, select(tick < next, 0x35, 0x55))); 12 | setCursorPosition(pos % 13 * 3, pos / 13 % 30); 13 | if rel < 10 { 14 | printChar(32); 15 | } 16 | printInt(rel); 17 | 18 | pos = pos + 1; 19 | next = tick + 1; 20 | } -------------------------------------------------------------------------------- /test/ges_test.cwa: -------------------------------------------------------------------------------- 1 | import "env.memory" memory(4); 2 | import "env.pow" fn pow(f32, f32) -> f32; 3 | import "env.sin" fn sin(f32) -> f32; 4 | import "env.cls" fn cls(i32); 5 | import "env.exp" fn exp(f32) -> f32; 6 | import "env.rectangle" fn rectangle(f32, f32, f32, f32, i32); 7 | 8 | include "../platform/src/ges.cwa" 9 | 10 | export fn snd(t: i32) -> f32 { 11 | sndGes(t) 12 | } 13 | 14 | export fn upd() { 15 | 80?0 = 32!32 / 200 & 2 | 0x41; 16 | 80?3 = (32!32 / 400)%8*12/7 + 40; 17 | let pulse = (32!32 * 256 / 2000) & 511; 18 | if pulse >= 256 { 19 | pulse = 511 - pulse; 20 | } 21 | 80?1 = pulse; 22 | 23 | cls(0); 24 | rectangle(0.0, 100.0, (pulse * 320 / 256) as f32, 16.0, 15); 25 | } 26 | 27 | data 80 { 28 | i8( 29 | 0x41, 0, 0, 80, 0x70, 0 30 | ) 31 | } -------------------------------------------------------------------------------- /test/log.cwa: -------------------------------------------------------------------------------- 1 | include "../examples/include/microw8-api.cwa" 2 | 3 | export fn upd() { 4 | printChar('\06f: '); 5 | printInt(32!32 * 6 / 100); 6 | printChar('\n\4'); 7 | } -------------------------------------------------------------------------------- /test/plot_ges.cwa: -------------------------------------------------------------------------------- 1 | include "../examples/include/microw8-api.cwa" 2 | 3 | export fn upd() { 4 | 80?0 = (32!32 >> 11 << 6) | 5; 5 | 80?1 = (sin(time() * 6 as f32) * 95 as f32) as i32 + 128; 6 | plotGes(); 7 | } 8 | 9 | data 80 { i8 ( 10 | 1, 128, 0, 69, 0, 15, 11 | 0, 0, 0, 0, 0, 0, 12 | 0, 0, 0, 0, 0, 0, 13 | 0, 0, 0, 0, 0, 0, 14 | 0xff, 0xff, 15 | 0xc1, 0, 0, 110, 0, 0 16 | ) } 17 | 18 | //import "env.gesSnd" fn gesSnd(i32) -> f32; 19 | 20 | include "../platform/src/ges.cwa" 21 | 22 | export fn snd(t: i32) -> f32 { 23 | gesSnd(t) 24 | } 25 | 26 | global mut samplePos: i32 = 0; 27 | 28 | const SoundBuffer = 0x30000; 29 | 30 | fn plotGes() { 31 | rectangle(0 as f32, 10 as f32, 320 as f32, 320 as f32, 0); 32 | let count = (time() * 44100 as f32) as i32 * 2 - samplePos; 33 | let i: i32; 34 | loop genLoop { 35 | (i*4)$SoundBuffer = gesSnd(samplePos + i); 36 | branch_if (i := i + 1) < count: genLoop; 37 | } 38 | samplePos = samplePos + count; 39 | 40 | let ch: i32; 41 | loop channelLoop { 42 | let offset = 159; 43 | i = 0; 44 | 45 | loop searchLoop { 46 | offset = offset + 1; 47 | branch_if (offset * 8 + ch - 8)$SoundBuffer < 0 as f32 | (offset * 8 + ch)$SoundBuffer >= 0 as f32 & offset + 160 < count: searchLoop; 48 | } 49 | 50 | offset = ch + (offset - 160) * 8; 51 | i = 0; 52 | loop plotLoop { 53 | setPixel(i, floor((i * 8 + offset)$SoundBuffer * 127 as f32) as i32 + 60 + ch * (120/8), 15); 54 | branch_if (i := i + 1) < 320: plotLoop; 55 | } 56 | 57 | branch_if (ch := ch + 8) < 16: channelLoop; 58 | } 59 | } -------------------------------------------------------------------------------- /test/start_fn.cwa: -------------------------------------------------------------------------------- 1 | include "../examples/include/microw8-api.cwa" 2 | 3 | export fn start() { 4 | printChar('Test'); 5 | } 6 | -------------------------------------------------------------------------------- /test/text_modes.cwa: -------------------------------------------------------------------------------- 1 | include "../examples/include/microw8-api.cwa" 2 | 3 | export fn upd() { 4 | printString(USER_MEM); 5 | } 6 | 7 | data USER_MEM { 8 | i8(12, 31, 5, 6) "Text mode" 9 | i8(5, 31, 4, 5) "Graphics mode" 10 | i8(6) "Console output\nSecond line\n" 11 | i8(4, 31, 4, 12) "Back to text mode" 12 | i8(0) 13 | } 14 | -------------------------------------------------------------------------------- /todo.txt: -------------------------------------------------------------------------------- 1 | * add support for 16bit sound (not just float) 2 | -------------------------------------------------------------------------------- /uw8-tool/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | *.wasm 3 | *.uw8 4 | -------------------------------------------------------------------------------- /uw8-tool/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "uw8-tool" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | wasmparser = "0.201" 10 | wasm-encoder = "0.201" 11 | walrus = { version = "0.20.3", default-features = false } 12 | anyhow = "1" 13 | pico-args = "0.5" 14 | upkr = { git = "https://github.com/exoticorn/upkr.git", rev = "080db40d0088bbee2bdf3c5c75288ac7853d6b7a" } 15 | pbr = "1" 16 | -------------------------------------------------------------------------------- /uw8-tool/src/filter_exports.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::path::Path; 3 | 4 | pub fn filter_exports(in_path: &Path, out_path: &Path) -> Result<()> { 5 | let mut module = walrus::Module::from_file(in_path)?; 6 | 7 | let exports_to_delete: Vec<_> = module 8 | .exports 9 | .iter() 10 | .filter_map(|export| match export.name.as_str() { 11 | "start" | "upd" | "snd" => None, 12 | _ => Some(export.id()), 13 | }) 14 | .collect(); 15 | 16 | for id in exports_to_delete { 17 | module.exports.delete(id); 18 | } 19 | 20 | walrus::passes::gc::run(&mut module); 21 | 22 | module.emit_wasm_file(out_path)?; 23 | 24 | Ok(()) 25 | } 26 | -------------------------------------------------------------------------------- /uw8-tool/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod base_module; 2 | mod filter_exports; 3 | mod pack; 4 | 5 | pub use base_module::BaseModule; 6 | pub use filter_exports::filter_exports; 7 | pub use pack::{pack, pack_file, unpack, unpack_file, PackConfig}; 8 | 9 | pub fn compressed_size(cart: &[u8]) -> f32 { 10 | if cart[0] != 2 { 11 | cart.len() as f32 12 | } else { 13 | upkr::compressed_size(&cart[1..]) + 1. 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /uw8-tool/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use anyhow::Result; 4 | use pico_args::Arguments; 5 | use uw8_tool::BaseModule; 6 | 7 | fn main() -> Result<()> { 8 | let mut args = Arguments::from_env(); 9 | 10 | if let Some(cmd) = args.subcommand()? { 11 | match cmd.as_str() { 12 | "make-base" => { 13 | let path: PathBuf = args.free_from_str()?; 14 | BaseModule::create_binary(&path)?; 15 | } 16 | "pack" => { 17 | let mut config = uw8_tool::PackConfig::default(); 18 | if args.contains(["-u", "--uncompressed"]) { 19 | config = config.uncompressed(); 20 | } 21 | let source: PathBuf = args.free_from_str()?; 22 | let dest: PathBuf = args.free_from_str()?; 23 | uw8_tool::pack_file(&source, &dest, &config)?; 24 | } 25 | "unpack" => { 26 | let source: PathBuf = args.free_from_str()?; 27 | let dest: PathBuf = args.free_from_str()?; 28 | uw8_tool::unpack_file(&source, &dest)?; 29 | } 30 | "filter-exports" => { 31 | let source: PathBuf = args.free_from_str()?; 32 | let dest: PathBuf = args.free_from_str()?; 33 | uw8_tool::filter_exports(&source, &dest)?; 34 | } 35 | "base-cwa" => { 36 | let path: PathBuf = args.free_from_str()?; 37 | BaseModule::for_format_version(1)?.write_as_cwa(path)?; 38 | } 39 | "base-wat" => { 40 | let path: PathBuf = args.free_from_str()?; 41 | BaseModule::for_format_version(1)?.write_as_wat(path)?; 42 | } 43 | _ => { 44 | eprintln!("Unknown subcommand '{}'", cmd); 45 | print_help(); 46 | } 47 | } 48 | } else { 49 | print_help(); 50 | } 51 | 52 | Ok(()) 53 | } 54 | 55 | fn print_help() { 56 | println!( 57 | "Usage: 58 | uw8-tool make-base 59 | uw8-tool pack 60 | uw8-tool unpack 61 | uw8-tool filter-exports " 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /uw8-window/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /uw8-window/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "uw8-window" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | env_logger = "0.11.3" 10 | winit = "0.28.6" 11 | log = "0.4" 12 | pico-args = "0.5" 13 | wgpu = "0.17" 14 | pollster = "0.3.0" 15 | bytemuck = { version = "1.15", features = [ "derive" ] } 16 | anyhow = "1" 17 | minifb = { version = "0.25.0", default-features = false, features = ["x11"] } 18 | winapi = { version = "0.3.9", features = [ "timeapi" ] } 19 | -------------------------------------------------------------------------------- /uw8-window/src/cpu.rs: -------------------------------------------------------------------------------- 1 | use std::time::Instant; 2 | 3 | use crate::{Input, WindowImpl}; 4 | use anyhow::Result; 5 | use minifb::{Key, WindowOptions}; 6 | 7 | static GAMEPAD_KEYS: &[Key] = &[ 8 | Key::Up, 9 | Key::Down, 10 | Key::Left, 11 | Key::Right, 12 | Key::Z, 13 | Key::X, 14 | Key::A, 15 | Key::S, 16 | ]; 17 | 18 | pub struct Window { 19 | window: minifb::Window, 20 | buffer: Vec, 21 | } 22 | 23 | impl Window { 24 | pub fn new() -> Result { 25 | #[cfg(target_os = "windows")] 26 | unsafe { 27 | winapi::um::timeapi::timeBeginPeriod(1); 28 | } 29 | 30 | let buffer: Vec = vec![0; 320 * 240]; 31 | 32 | let options = WindowOptions { 33 | scale: minifb::Scale::X2, 34 | scale_mode: minifb::ScaleMode::AspectRatioStretch, 35 | resize: true, 36 | ..Default::default() 37 | }; 38 | let window = minifb::Window::new("MicroW8", 320, 240, options).unwrap(); 39 | 40 | Ok(Window { window, buffer }) 41 | } 42 | } 43 | 44 | impl WindowImpl for Window { 45 | fn begin_frame(&mut self) -> Input { 46 | let mut gamepads = [0u8; 4]; 47 | for key in self.window.get_keys() { 48 | if let Some(index) = GAMEPAD_KEYS 49 | .iter() 50 | .enumerate() 51 | .find(|(_, &k)| k == key) 52 | .map(|(i, _)| i) 53 | { 54 | gamepads[0] |= 1 << index; 55 | } 56 | } 57 | 58 | Input { 59 | gamepads, 60 | reset: self.window.is_key_pressed(Key::R, minifb::KeyRepeat::No), 61 | } 62 | } 63 | 64 | fn end_frame(&mut self, framebuffer: &[u8], palette: &[u8], next_frame: Instant) { 65 | for (i, &color_index) in framebuffer.iter().enumerate() { 66 | let offset = color_index as usize * 4; 67 | self.buffer[i] = 0xff000000 68 | | ((palette[offset] as u32) << 16) 69 | | ((palette[offset + 1] as u32) << 8) 70 | | palette[offset + 2] as u32; 71 | } 72 | self.window 73 | .update_with_buffer(&self.buffer, 320, 240) 74 | .unwrap(); 75 | if let Some(sleep) = next_frame.checked_duration_since(Instant::now()) { 76 | std::thread::sleep(sleep); 77 | } 78 | } 79 | 80 | fn is_open(&self) -> bool { 81 | self.window.is_open() && !self.window.is_key_down(Key::Escape) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /uw8-window/src/gpu/crt.rs: -------------------------------------------------------------------------------- 1 | use wgpu::util::DeviceExt; 2 | use winit::dpi::PhysicalSize; 3 | 4 | use super::{scale_mode::ScaleMode, Filter}; 5 | 6 | pub struct CrtFilter { 7 | uniform_buffer: wgpu::Buffer, 8 | bind_group: wgpu::BindGroup, 9 | pipeline: wgpu::RenderPipeline, 10 | } 11 | 12 | impl CrtFilter { 13 | pub fn new( 14 | device: &wgpu::Device, 15 | screen: &wgpu::TextureView, 16 | resolution: PhysicalSize, 17 | surface_format: wgpu::TextureFormat, 18 | scale_mode: ScaleMode, 19 | ) -> CrtFilter { 20 | let uniforms = Uniforms { 21 | texture_scale: scale_mode.texture_scale_from_resolution(resolution), 22 | }; 23 | 24 | let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { 25 | label: None, 26 | contents: bytemuck::cast_slice(&[uniforms]), 27 | usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, 28 | }); 29 | 30 | let crt_bind_group_layout = 31 | device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 32 | entries: &[ 33 | wgpu::BindGroupLayoutEntry { 34 | binding: 0, 35 | visibility: wgpu::ShaderStages::FRAGMENT, 36 | ty: wgpu::BindingType::Texture { 37 | multisampled: false, 38 | view_dimension: wgpu::TextureViewDimension::D2, 39 | sample_type: wgpu::TextureSampleType::Float { filterable: false }, 40 | }, 41 | count: None, 42 | }, 43 | wgpu::BindGroupLayoutEntry { 44 | binding: 1, 45 | visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, 46 | ty: wgpu::BindingType::Buffer { 47 | ty: wgpu::BufferBindingType::Uniform, 48 | has_dynamic_offset: false, 49 | min_binding_size: None, 50 | }, 51 | count: None, 52 | }, 53 | ], 54 | label: None, 55 | }); 56 | 57 | let crt_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { 58 | layout: &crt_bind_group_layout, 59 | entries: &[ 60 | wgpu::BindGroupEntry { 61 | binding: 0, 62 | resource: wgpu::BindingResource::TextureView(&screen), 63 | }, 64 | wgpu::BindGroupEntry { 65 | binding: 1, 66 | resource: uniform_buffer.as_entire_binding(), 67 | }, 68 | ], 69 | label: None, 70 | }); 71 | 72 | let crt_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { 73 | label: None, 74 | source: wgpu::ShaderSource::Wgsl(include_str!("crt.wgsl").into()), 75 | }); 76 | 77 | let render_pipeline_layout = 78 | device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 79 | label: None, 80 | bind_group_layouts: &[&crt_bind_group_layout], 81 | push_constant_ranges: &[], 82 | }); 83 | 84 | let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 85 | label: None, 86 | layout: Some(&render_pipeline_layout), 87 | vertex: wgpu::VertexState { 88 | module: &crt_shader, 89 | entry_point: "vs_main", 90 | buffers: &[], 91 | }, 92 | fragment: Some(wgpu::FragmentState { 93 | module: &crt_shader, 94 | entry_point: "fs_main", 95 | targets: &[Some(wgpu::ColorTargetState { 96 | format: surface_format, 97 | blend: None, 98 | write_mask: wgpu::ColorWrites::ALL, 99 | })], 100 | }), 101 | primitive: Default::default(), 102 | depth_stencil: None, 103 | multisample: Default::default(), 104 | multiview: None, 105 | }); 106 | 107 | CrtFilter { 108 | uniform_buffer, 109 | bind_group: crt_bind_group, 110 | pipeline: render_pipeline, 111 | } 112 | } 113 | } 114 | 115 | impl Filter for CrtFilter { 116 | fn resize(&mut self, queue: &wgpu::Queue, new_size: PhysicalSize, scale_mode: ScaleMode) { 117 | let uniforms = Uniforms { 118 | texture_scale: scale_mode.texture_scale_from_resolution(new_size), 119 | }; 120 | queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms])); 121 | } 122 | 123 | fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { 124 | render_pass.set_pipeline(&self.pipeline); 125 | render_pass.set_bind_group(0, &self.bind_group, &[]); 126 | render_pass.draw(0..6, 0..1); 127 | } 128 | } 129 | 130 | #[repr(C)] 131 | #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] 132 | struct Uniforms { 133 | texture_scale: [f32; 4], 134 | } 135 | -------------------------------------------------------------------------------- /uw8-window/src/gpu/crt.wgsl: -------------------------------------------------------------------------------- 1 | struct VertexOutput { 2 | @builtin(position) clip_position: vec4, 3 | @location(0) tex_coords: vec2, 4 | } 5 | 6 | struct Uniforms { 7 | texture_scale: vec4, 8 | } 9 | 10 | @group(0) @binding(1) var uniforms: Uniforms; 11 | 12 | @vertex 13 | fn vs_main( 14 | @builtin(vertex_index) in_vertex_index: u32, 15 | ) -> VertexOutput { 16 | var out: VertexOutput; 17 | let i = in_vertex_index / 3u + in_vertex_index % 3u; 18 | let x = -1.0 + f32(i % 2u) * 322.0; 19 | let y = -1.0 + f32(i / 2u) * 242.0; 20 | out.clip_position = vec4((vec2(x, y) - vec2(160.0, 120.0)) * uniforms.texture_scale.xy, 0.0, 1.0); 21 | out.tex_coords = vec2(x, y); 22 | return out; 23 | } 24 | 25 | @group(0) @binding(0) var screen_texture: texture_2d; 26 | 27 | fn sample_pixel(coords: vec2, offset: vec4) -> vec3 { 28 | let is_outside = any(vec2(coords) >= vec2(320u, 240u)); 29 | if(is_outside) { 30 | return vec3(0.0); 31 | } else { 32 | let f = max(vec4(0.008) / offset - vec4(0.0024), vec4(0.0)); 33 | return textureLoad(screen_texture, coords, 0).rgb * (f.x + f.y + f.z + f.w); 34 | } 35 | } 36 | 37 | @fragment 38 | fn fs_main(in: VertexOutput) -> @location(0) vec4 { 39 | let pixelf = floor(in.tex_coords); 40 | let o = vec2(0.5) - (in.tex_coords - pixelf); 41 | let pixel = vec2(pixelf); 42 | 43 | let offset_x = o.xxxx + vec4(-0.125, 0.375, 0.125, -0.375) * uniforms.texture_scale.z; 44 | let offset_y = o.yyyy + vec4(-0.375, -0.125, 0.375, 0.125) * uniforms.texture_scale.z; 45 | 46 | var offset_x0 = max(abs(offset_x + vec4(-1.0)) - vec4(0.5), vec4(0.0)); 47 | var offset_x1 = max(abs(offset_x) - vec4(0.5), vec4(0.0)); 48 | var offset_x2 = max(abs(offset_x + vec4(1.0)) - vec4(0.5), vec4(0.0)); 49 | 50 | offset_x0 = offset_x0 * offset_x0; 51 | offset_x1 = offset_x1 * offset_x1; 52 | offset_x2 = offset_x2 * offset_x2; 53 | 54 | var offset_yr = offset_y + vec4(-1.0); 55 | offset_yr = vec4(0.02) + offset_yr * offset_yr; 56 | 57 | var acc = sample_pixel(pixel + vec2(-1, -1), offset_x0 + offset_yr); 58 | acc = acc + sample_pixel(pixel + vec2(0, -1), offset_x1 + offset_yr); 59 | acc = acc + sample_pixel(pixel + vec2(1, -1), offset_x2 + offset_yr); 60 | 61 | offset_yr = vec4(0.02) + offset_y * offset_y; 62 | 63 | acc = acc + sample_pixel(pixel + vec2(-1, 0), offset_x0 + offset_yr); 64 | acc = acc + sample_pixel(pixel, offset_x1 + offset_yr); 65 | acc = acc + sample_pixel(pixel + vec2(1, 0), offset_x2 + offset_yr); 66 | 67 | offset_yr = offset_y + vec4(1.0); 68 | offset_yr = vec4(0.02) + offset_yr * offset_yr; 69 | 70 | acc = acc + sample_pixel(pixel + vec2(-1, 1), offset_x0 + offset_yr); 71 | acc = acc + sample_pixel(pixel + vec2(0, 1), offset_x1 + offset_yr); 72 | acc = acc + sample_pixel(pixel + vec2(1, 1), offset_x2 + offset_yr); 73 | 74 | return vec4(acc, 1.0); 75 | } 76 | -------------------------------------------------------------------------------- /uw8-window/src/gpu/fast_crt.rs: -------------------------------------------------------------------------------- 1 | use wgpu::util::DeviceExt; 2 | use winit::dpi::PhysicalSize; 3 | 4 | use super::{scale_mode::ScaleMode, Filter}; 5 | 6 | pub struct FastCrtFilter { 7 | uniform_buffer: wgpu::Buffer, 8 | bind_group: wgpu::BindGroup, 9 | pipeline: wgpu::RenderPipeline, 10 | } 11 | 12 | impl FastCrtFilter { 13 | pub fn new( 14 | device: &wgpu::Device, 15 | screen: &wgpu::TextureView, 16 | resolution: PhysicalSize, 17 | surface_format: wgpu::TextureFormat, 18 | chromatic: bool, 19 | scale_mode: ScaleMode, 20 | ) -> FastCrtFilter { 21 | let uniforms = Uniforms { 22 | texture_scale: scale_mode.texture_scale_from_resolution(resolution), 23 | }; 24 | 25 | let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { 26 | label: None, 27 | contents: bytemuck::cast_slice(&[uniforms]), 28 | usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, 29 | }); 30 | 31 | let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 32 | entries: &[ 33 | wgpu::BindGroupLayoutEntry { 34 | binding: 0, 35 | visibility: wgpu::ShaderStages::FRAGMENT, 36 | ty: wgpu::BindingType::Texture { 37 | multisampled: false, 38 | view_dimension: wgpu::TextureViewDimension::D2, 39 | sample_type: wgpu::TextureSampleType::Float { filterable: true }, 40 | }, 41 | count: None, 42 | }, 43 | wgpu::BindGroupLayoutEntry { 44 | binding: 1, 45 | visibility: wgpu::ShaderStages::FRAGMENT, 46 | ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), 47 | count: None, 48 | }, 49 | wgpu::BindGroupLayoutEntry { 50 | binding: 2, 51 | visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, 52 | ty: wgpu::BindingType::Buffer { 53 | ty: wgpu::BufferBindingType::Uniform, 54 | has_dynamic_offset: false, 55 | min_binding_size: None, 56 | }, 57 | count: None, 58 | }, 59 | ], 60 | label: None, 61 | }); 62 | 63 | let sampler = device.create_sampler(&wgpu::SamplerDescriptor { 64 | mag_filter: wgpu::FilterMode::Linear, 65 | ..Default::default() 66 | }); 67 | 68 | let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { 69 | layout: &bind_group_layout, 70 | entries: &[ 71 | wgpu::BindGroupEntry { 72 | binding: 0, 73 | resource: wgpu::BindingResource::TextureView(&screen), 74 | }, 75 | wgpu::BindGroupEntry { 76 | binding: 1, 77 | resource: wgpu::BindingResource::Sampler(&sampler), 78 | }, 79 | wgpu::BindGroupEntry { 80 | binding: 2, 81 | resource: uniform_buffer.as_entire_binding(), 82 | }, 83 | ], 84 | label: None, 85 | }); 86 | 87 | let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { 88 | label: None, 89 | source: wgpu::ShaderSource::Wgsl(include_str!("fast_crt.wgsl").into()), 90 | }); 91 | 92 | let render_pipeline_layout = 93 | device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 94 | label: None, 95 | bind_group_layouts: &[&bind_group_layout], 96 | push_constant_ranges: &[], 97 | }); 98 | 99 | let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 100 | label: None, 101 | layout: Some(&render_pipeline_layout), 102 | vertex: wgpu::VertexState { 103 | module: &shader, 104 | entry_point: "vs_main", 105 | buffers: &[], 106 | }, 107 | fragment: Some(wgpu::FragmentState { 108 | module: &shader, 109 | entry_point: if chromatic { 110 | "fs_main_chromatic" 111 | } else { 112 | "fs_main" 113 | }, 114 | targets: &[Some(wgpu::ColorTargetState { 115 | format: surface_format, 116 | blend: None, 117 | write_mask: wgpu::ColorWrites::ALL, 118 | })], 119 | }), 120 | primitive: Default::default(), 121 | depth_stencil: None, 122 | multisample: Default::default(), 123 | multiview: None, 124 | }); 125 | 126 | FastCrtFilter { 127 | uniform_buffer, 128 | bind_group, 129 | pipeline: render_pipeline, 130 | } 131 | } 132 | } 133 | 134 | impl Filter for FastCrtFilter { 135 | fn resize(&mut self, queue: &wgpu::Queue, new_size: PhysicalSize, scale_mode: ScaleMode) { 136 | let uniforms = Uniforms { 137 | texture_scale: scale_mode.texture_scale_from_resolution(new_size), 138 | }; 139 | queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms])); 140 | } 141 | 142 | fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { 143 | render_pass.set_pipeline(&self.pipeline); 144 | render_pass.set_bind_group(0, &self.bind_group, &[]); 145 | render_pass.draw(0..6, 0..1); 146 | } 147 | } 148 | 149 | #[repr(C)] 150 | #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] 151 | struct Uniforms { 152 | texture_scale: [f32; 4], 153 | } 154 | -------------------------------------------------------------------------------- /uw8-window/src/gpu/fast_crt.wgsl: -------------------------------------------------------------------------------- 1 | struct VertexOutput { 2 | @builtin(position) clip_position: vec4, 3 | @location(0) tex_coords: vec2, 4 | } 5 | 6 | struct Uniforms { 7 | texture_scale: vec4, 8 | } 9 | 10 | @group(0) @binding(2) var uniforms: Uniforms; 11 | 12 | @vertex 13 | fn vs_main( 14 | @builtin(vertex_index) in_vertex_index: u32, 15 | ) -> VertexOutput { 16 | var out: VertexOutput; 17 | let i = in_vertex_index / 3u + in_vertex_index % 3u; 18 | let x = 0.0 + f32(i % 2u) * 320.0; 19 | let y = 0.0 + f32(i / 2u) * 240.0; 20 | out.clip_position = vec4((vec2(x, y) - vec2(160.0, 120.0)) * uniforms.texture_scale.xy, 0.0, 1.0); 21 | out.tex_coords = vec2(x, y); 22 | return out; 23 | } 24 | 25 | @group(0) @binding(0) var screen_texture: texture_2d; 26 | @group(0) @binding(1) var linear_sampler: sampler; 27 | 28 | fn row_factor(offset: f32) -> f32 { 29 | return 1.0 / (1.0 + offset * offset * 16.0); 30 | } 31 | 32 | fn col_factor(offset: f32) -> f32 { 33 | let o = max(0.0, abs(offset) - 0.4); 34 | return 1.0 / (1.0 + o * o * 16.0); 35 | } 36 | 37 | fn sample_screen(tex_coords: vec2) -> vec4 { 38 | let base = round(tex_coords) - vec2(0.5); 39 | let frac = tex_coords - base; 40 | 41 | let top_factor = row_factor(frac.y); 42 | let bottom_factor = row_factor(frac.y - 1.0); 43 | 44 | let v = base.y + bottom_factor / (bottom_factor + top_factor); 45 | 46 | let left_factor = col_factor(frac.x); 47 | let right_factor = col_factor(frac.x - 1.0); 48 | 49 | let u = base.x + right_factor / (right_factor + left_factor); 50 | 51 | return textureSample(screen_texture, linear_sampler, vec2(u, v) / vec2(320.0, 240.0)) * (top_factor + bottom_factor) * (left_factor + right_factor) * 1.1; 52 | } 53 | 54 | @fragment 55 | fn fs_main(in: VertexOutput) -> @location(0) vec4 { 56 | return sample_screen(in.tex_coords); 57 | } 58 | 59 | @fragment 60 | fn fs_main_chromatic(in: VertexOutput) -> @location(0) vec4 { 61 | let r = sample_screen(in.tex_coords + vec2(0.2, 0.2)).r; 62 | let g = sample_screen(in.tex_coords + vec2(0.07, -0.27)).g; 63 | let b = sample_screen(in.tex_coords + vec2(-0.27, 0.07)).b; 64 | return vec4(r, g, b, 1.0); 65 | } 66 | 67 | -------------------------------------------------------------------------------- /uw8-window/src/gpu/palette.wgsl: -------------------------------------------------------------------------------- 1 | struct VertexOutput { 2 | @builtin(position) clip_position: vec4, 3 | @location(0) tex_coords: vec2, 4 | } 5 | 6 | @vertex 7 | fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { 8 | var out: VertexOutput; 9 | let x = (1.0 - f32(vertex_index)) * 3.0; 10 | let y = f32(vertex_index & 1u) * 3.0 - 1.0; 11 | out.clip_position = vec4(x, y, 0.0, 1.0); 12 | out.tex_coords = vec2((x + 1.0) * 160.0, (y + 1.0) * 120.0); 13 | return out; 14 | } 15 | 16 | @group(0) @binding(0) var framebuffer_texture: texture_2d; 17 | @group(0) @binding(1) var palette_texture: texture_1d; 18 | 19 | @fragment 20 | fn fs_main(in: VertexOutput) -> @location(0) vec4 { 21 | let texel = vec2(floor(in.tex_coords)); 22 | let index = textureLoad(framebuffer_texture, texel, 0).r; 23 | return textureLoad(palette_texture, i32(index), 0); 24 | } -------------------------------------------------------------------------------- /uw8-window/src/gpu/scale_mode.rs: -------------------------------------------------------------------------------- 1 | use winit::dpi::PhysicalSize; 2 | 3 | #[derive(Debug, Copy, Clone)] 4 | pub enum ScaleMode { 5 | Fit, 6 | Fill, 7 | } 8 | 9 | impl Default for ScaleMode { 10 | fn default() -> ScaleMode { 11 | ScaleMode::Fit 12 | } 13 | } 14 | 15 | impl ScaleMode { 16 | pub fn texture_scale_from_resolution(&self, res: PhysicalSize) -> [f32; 4] { 17 | let scale = match self { 18 | ScaleMode::Fit => ((res.width as f32) / 160.0).min((res.height as f32) / 120.0), 19 | ScaleMode::Fill => ((res.width as f32) / 160.0).max((res.height as f32) / 120.0), 20 | }; 21 | [ 22 | scale / res.width as f32, 23 | scale / res.height as f32, 24 | 2.0 / scale, 25 | 0.0, 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /uw8-window/src/gpu/square.rs: -------------------------------------------------------------------------------- 1 | use wgpu::util::DeviceExt; 2 | use winit::dpi::PhysicalSize; 3 | 4 | use super::{scale_mode::ScaleMode, Filter}; 5 | 6 | pub struct SquareFilter { 7 | uniform_buffer: wgpu::Buffer, 8 | bind_group: wgpu::BindGroup, 9 | pipeline: wgpu::RenderPipeline, 10 | } 11 | 12 | impl SquareFilter { 13 | pub fn new( 14 | device: &wgpu::Device, 15 | screen: &wgpu::TextureView, 16 | resolution: PhysicalSize, 17 | surface_format: wgpu::TextureFormat, 18 | scale_mode: ScaleMode, 19 | ) -> SquareFilter { 20 | let uniforms = Uniforms { 21 | texture_scale: scale_mode.texture_scale_from_resolution(resolution), 22 | }; 23 | 24 | let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { 25 | label: None, 26 | contents: bytemuck::cast_slice(&[uniforms]), 27 | usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, 28 | }); 29 | 30 | let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 31 | entries: &[ 32 | wgpu::BindGroupLayoutEntry { 33 | binding: 0, 34 | visibility: wgpu::ShaderStages::FRAGMENT, 35 | ty: wgpu::BindingType::Texture { 36 | multisampled: false, 37 | view_dimension: wgpu::TextureViewDimension::D2, 38 | sample_type: wgpu::TextureSampleType::Float { filterable: true }, 39 | }, 40 | count: None, 41 | }, 42 | wgpu::BindGroupLayoutEntry { 43 | binding: 1, 44 | visibility: wgpu::ShaderStages::FRAGMENT, 45 | ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), 46 | count: None, 47 | }, 48 | wgpu::BindGroupLayoutEntry { 49 | binding: 2, 50 | visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, 51 | ty: wgpu::BindingType::Buffer { 52 | ty: wgpu::BufferBindingType::Uniform, 53 | has_dynamic_offset: false, 54 | min_binding_size: None, 55 | }, 56 | count: None, 57 | }, 58 | ], 59 | label: None, 60 | }); 61 | 62 | let sampler = device.create_sampler(&wgpu::SamplerDescriptor { 63 | mag_filter: wgpu::FilterMode::Linear, 64 | ..Default::default() 65 | }); 66 | 67 | let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { 68 | layout: &bind_group_layout, 69 | entries: &[ 70 | wgpu::BindGroupEntry { 71 | binding: 0, 72 | resource: wgpu::BindingResource::TextureView(&screen), 73 | }, 74 | wgpu::BindGroupEntry { 75 | binding: 1, 76 | resource: wgpu::BindingResource::Sampler(&sampler), 77 | }, 78 | wgpu::BindGroupEntry { 79 | binding: 2, 80 | resource: uniform_buffer.as_entire_binding(), 81 | }, 82 | ], 83 | label: None, 84 | }); 85 | 86 | let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { 87 | label: None, 88 | source: wgpu::ShaderSource::Wgsl(include_str!("square.wgsl").into()), 89 | }); 90 | 91 | let render_pipeline_layout = 92 | device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 93 | label: None, 94 | bind_group_layouts: &[&bind_group_layout], 95 | push_constant_ranges: &[], 96 | }); 97 | 98 | let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 99 | label: None, 100 | layout: Some(&render_pipeline_layout), 101 | vertex: wgpu::VertexState { 102 | module: &shader, 103 | entry_point: "vs_main", 104 | buffers: &[], 105 | }, 106 | fragment: Some(wgpu::FragmentState { 107 | module: &shader, 108 | entry_point: "fs_main", 109 | targets: &[Some(wgpu::ColorTargetState { 110 | format: surface_format, 111 | blend: None, 112 | write_mask: wgpu::ColorWrites::ALL, 113 | })], 114 | }), 115 | primitive: Default::default(), 116 | depth_stencil: None, 117 | multisample: Default::default(), 118 | multiview: None, 119 | }); 120 | 121 | SquareFilter { 122 | uniform_buffer, 123 | bind_group, 124 | pipeline: render_pipeline, 125 | } 126 | } 127 | } 128 | 129 | impl Filter for SquareFilter { 130 | fn resize(&mut self, queue: &wgpu::Queue, new_size: PhysicalSize, scale_mode: ScaleMode) { 131 | let uniforms = Uniforms { 132 | texture_scale: scale_mode.texture_scale_from_resolution(new_size), 133 | }; 134 | queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms])); 135 | } 136 | 137 | fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { 138 | render_pass.set_pipeline(&self.pipeline); 139 | render_pass.set_bind_group(0, &self.bind_group, &[]); 140 | render_pass.draw(0..6, 0..1); 141 | } 142 | } 143 | 144 | #[repr(C)] 145 | #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] 146 | struct Uniforms { 147 | texture_scale: [f32; 4], 148 | } 149 | -------------------------------------------------------------------------------- /uw8-window/src/gpu/square.wgsl: -------------------------------------------------------------------------------- 1 | struct VertexOutput { 2 | @builtin(position) clip_position: vec4, 3 | @location(0) tex_coords: vec2, 4 | } 5 | 6 | struct Uniforms { 7 | texture_scale: vec4, 8 | } 9 | 10 | @group(0) @binding(2) var uniforms: Uniforms; 11 | 12 | @vertex 13 | fn vs_main( 14 | @builtin(vertex_index) in_vertex_index: u32, 15 | ) -> VertexOutput { 16 | var out: VertexOutput; 17 | let i = in_vertex_index / 3u + in_vertex_index % 3u; 18 | let x = 0.0 + f32(i % 2u) * 320.0; 19 | let y = 0.0 + f32(i / 2u) * 240.0; 20 | out.clip_position = vec4((vec2(x, y) - vec2(160.0, 120.0)) * uniforms.texture_scale.xy, 0.0, 1.0); 21 | out.tex_coords = vec2(x, y); 22 | return out; 23 | } 24 | 25 | @group(0) @binding(0) var screen_texture: texture_2d; 26 | @group(0) @binding(1) var linear_sampler: sampler; 27 | 28 | fn aa_tex_coord(c: f32) -> f32 { 29 | let low = c - uniforms.texture_scale.z * 0.5; 30 | let high = c + uniforms.texture_scale.z * 0.5; 31 | let base = floor(low); 32 | let center = base + 0.5; 33 | let next = base + 1.0; 34 | if high > next { 35 | return center + (high - next) / (high - low); 36 | } else { 37 | return center; 38 | } 39 | } 40 | 41 | @fragment 42 | fn fs_main(in: VertexOutput) -> @location(0) vec4 { 43 | return textureSample(screen_texture, linear_sampler, vec2(aa_tex_coord(in.tex_coords.x), aa_tex_coord(in.tex_coords.y)) / vec2(320.0, 240.0)); 44 | } 45 | -------------------------------------------------------------------------------- /uw8-window/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use gpu::scale_mode::ScaleMode; 3 | use std::time::Instant; 4 | 5 | mod cpu; 6 | mod gpu; 7 | 8 | pub struct Window { 9 | inner: Box, 10 | fps_counter: Option, 11 | } 12 | 13 | struct FpsCounter { 14 | start: Instant, 15 | num_frames: u32, 16 | } 17 | 18 | impl Window { 19 | pub fn new(mut config: WindowConfig) -> Result { 20 | let fps_counter = if config.fps_counter { 21 | Some(FpsCounter { 22 | start: Instant::now(), 23 | num_frames: 0, 24 | }) 25 | } else { 26 | None 27 | }; 28 | config.scale = config.scale.max(1.).min(20.); 29 | if config.enable_gpu { 30 | match gpu::Window::new(config) { 31 | Ok(window) => { 32 | return Ok(Window { 33 | inner: Box::new(window), 34 | fps_counter, 35 | }) 36 | } 37 | Err(err) => eprintln!( 38 | "Failed to create gpu window: {}\nFalling back tp cpu window", 39 | err 40 | ), 41 | } 42 | } 43 | cpu::Window::new().map(|window| Window { 44 | inner: Box::new(window), 45 | fps_counter, 46 | }) 47 | } 48 | 49 | pub fn begin_frame(&mut self) -> Input { 50 | self.inner.begin_frame() 51 | } 52 | pub fn end_frame(&mut self, framebuffer: &[u8], palette: &[u8], next_frame: Instant) { 53 | self.inner.end_frame(framebuffer, palette, next_frame); 54 | if let Some(ref mut fps_counter) = self.fps_counter { 55 | fps_counter.num_frames += 1; 56 | let elapsed = fps_counter.start.elapsed().as_secs_f32(); 57 | if elapsed >= 1.0 { 58 | println!("fps: {:.1}", fps_counter.num_frames as f32 / elapsed); 59 | fps_counter.num_frames = 0; 60 | fps_counter.start = Instant::now(); 61 | } 62 | } 63 | } 64 | 65 | pub fn is_open(&self) -> bool { 66 | self.inner.is_open() 67 | } 68 | } 69 | 70 | #[derive(Debug)] 71 | pub struct WindowConfig { 72 | enable_gpu: bool, 73 | filter: u32, 74 | fullscreen: bool, 75 | fps_counter: bool, 76 | scale: f32, 77 | scale_mode: ScaleMode, 78 | } 79 | 80 | impl Default for WindowConfig { 81 | fn default() -> WindowConfig { 82 | WindowConfig { 83 | enable_gpu: true, 84 | filter: 5, 85 | fullscreen: false, 86 | fps_counter: false, 87 | scale: 2., 88 | scale_mode: ScaleMode::Fit, 89 | } 90 | } 91 | } 92 | 93 | impl WindowConfig { 94 | pub fn parse_arguments(&mut self, args: &mut pico_args::Arguments) { 95 | self.enable_gpu = !args.contains("--no-gpu"); 96 | if let Some(filter) = args.opt_value_from_str::<_, String>("--filter").unwrap() { 97 | self.filter = match filter.as_str() { 98 | "1" | "nearest" => 1, 99 | "2" | "fast_crt" => 2, 100 | "3" | "ss_crt" => 3, 101 | "4" | "chromatic" => 4, 102 | "5" | "auto_crt" => 5, 103 | o => { 104 | println!("Unknown --filter '{}'", o); 105 | std::process::exit(1); 106 | } 107 | } 108 | } 109 | self.fullscreen = args.contains("--fullscreen"); 110 | self.fps_counter = args.contains("--fps"); 111 | self.scale = args 112 | .opt_value_from_str("--scale") 113 | .unwrap() 114 | .unwrap_or(self.scale); 115 | if args.contains("--scale-fill") { 116 | self.scale_mode = ScaleMode::Fill; 117 | } 118 | } 119 | } 120 | 121 | pub struct Input { 122 | pub gamepads: [u8; 4], 123 | pub reset: bool, 124 | } 125 | 126 | trait WindowImpl { 127 | fn begin_frame(&mut self) -> Input; 128 | fn end_frame(&mut self, framebuffer: &[u8], palette: &[u8], next_frame: Instant); 129 | fn is_open(&self) -> bool; 130 | } 131 | -------------------------------------------------------------------------------- /uw8-window/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::Instant; 2 | use uw8_window::WindowConfig; 3 | 4 | fn main() { 5 | env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); 6 | 7 | let mut args = pico_args::Arguments::from_env(); 8 | 9 | let mut framebuffer = vec![0u8; 320 * 240]; 10 | let mut start_time = Instant::now(); 11 | 12 | let mut palette = vec![0u32; 256]; 13 | for i in 0..256 { 14 | let v = i & 15; 15 | let r = ((i >> 2) & 12) * v; 16 | let g = ((i >> 3) & 12) * v; 17 | let b = ((i >> 4) & 12) * v; 18 | palette[i as usize] = r + (g << 8) + (b << 16); 19 | } 20 | 21 | let mut fps_start = Instant::now(); 22 | let mut fps_counter = 0; 23 | 24 | let mut window_config = WindowConfig::default(); 25 | window_config.parse_arguments(&mut args); 26 | 27 | let mut window = uw8_window::Window::new(window_config).unwrap(); 28 | 29 | while window.is_open() { 30 | let input = window.begin_frame(); 31 | if input.reset { 32 | start_time = Instant::now(); 33 | } 34 | draw_frame(&mut framebuffer, start_time.elapsed().as_secs_f32()); 35 | window.end_frame(&framebuffer, bytemuck::cast_slice(&palette), Instant::now()); 36 | 37 | fps_counter += 1; 38 | let elapsed = fps_start.elapsed().as_secs_f32(); 39 | if elapsed >= 1.0 { 40 | println!("{:.1} fps", fps_counter as f32 / elapsed); 41 | fps_start = Instant::now(); 42 | fps_counter = 0; 43 | } 44 | } 45 | } 46 | 47 | fn draw_frame(framebuffer: &mut [u8], time: f32) { 48 | for x in 0..320 { 49 | let xr = x as f32 - 160.0; 50 | for y in 0..240 { 51 | let yr = y as f32 - 120.0; 52 | let f = 8192.0 / (xr * xr + yr * yr); 53 | let u = xr * f + 512.0 + time * 32.0; 54 | let v = yr * f + time * 29.0; 55 | let c = (u.floor() as i32 ^ v.floor() as i32) as u32; 56 | framebuffer[x + y * 320] = c as u8; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | .parcel-cache/ 2 | dist/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /web/build-run-web: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rm -rf .parcel-cache && yarn parcel build src/run-web.html && cp dist/run-web.html ../src/ 3 | -------------------------------------------------------------------------------- /web/opus-repro.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 57 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "Unlicense", 3 | "devDependencies": { 4 | "@parcel/optimizer-data-url": "^2.0.0", 5 | "@parcel/transformer-inline-string": "^2.0.0", 6 | "parcel": "^2.0.0" 7 | }, 8 | "dependencies": {} 9 | } 10 | -------------------------------------------------------------------------------- /web/run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rm -rf .parcel-cache && yarn parcel src/index.html 3 | -------------------------------------------------------------------------------- /web/src/audiolet.js: -------------------------------------------------------------------------------- 1 | let U8 = (...a) => new Uint8Array(...a); 2 | class APU extends AudioWorkletProcessor { 3 | constructor() { 4 | super(); 5 | this.sampleIndex = 0; 6 | this.currentTime = 0; 7 | this.isFirstMessage = true; 8 | this.pendingUpdates = []; 9 | this.port.onmessage = (ev) => { 10 | if(this.memory) { 11 | if(this.isFirstMessage) 12 | { 13 | this.currentTime += (ev.data.t - this.currentTime) / 8; 14 | this.isFirstMessage = false; 15 | } 16 | this.pendingUpdates.push(ev.data); 17 | } else { 18 | this.load(ev.data[0], ev.data[1]); 19 | } 20 | }; 21 | } 22 | 23 | async load(platform_data, data) { 24 | let memory = new WebAssembly.Memory({ initial: 4, maximum: 4 }); 25 | 26 | let importObject = { 27 | env: { 28 | memory 29 | }, 30 | }; 31 | 32 | for (let n of ['acos', 'asin', 'atan', 'atan2', 'cos', 'exp', 'log', 'sin', 'tan', 'pow']) { 33 | importObject.env[n] = Math[n]; 34 | } 35 | 36 | for (let i = 9; i < 64; ++i) { 37 | importObject.env['reserved' + i] = () => { }; 38 | } 39 | 40 | let logLine = ''; 41 | importObject.env['logChar'] = (c) => { 42 | if(c == 10) { 43 | console.log(logLine); 44 | logLine = ''; 45 | } else { 46 | logLine += String.fromCharCode(c); 47 | } 48 | }; 49 | 50 | for (let i = 0; i < 16; ++i) { 51 | importObject.env['g_reserved' + i] = 0; 52 | } 53 | 54 | let instantiate = async (data) => (await WebAssembly.instantiate(data, importObject)).instance; 55 | 56 | let platform_instance = await instantiate(platform_data); 57 | 58 | for (let name in platform_instance.exports) { 59 | importObject.env[name] = platform_instance.exports[name] 60 | } 61 | 62 | let instance = await instantiate(data); 63 | 64 | this.memory = memory; 65 | 66 | this.snd = instance.exports.snd || platform_instance.exports.sndGes; 67 | 68 | this.port.postMessage(2); 69 | } 70 | 71 | process(inputs, outputs, parameters) { 72 | this.isFirstMessage = true; 73 | if(this.snd) { 74 | while(this.pendingUpdates.length > 0 && this.pendingUpdates[0].t <= this.currentTime) { 75 | U8(this.memory.buffer, 80, 32).set(U8(this.pendingUpdates.shift().r)); 76 | } 77 | let u32Mem = new Uint32Array(this.memory.buffer); 78 | u32Mem[16] = this.currentTime; 79 | let channels = outputs[0]; 80 | let index = this.sampleIndex; 81 | let numSamples = channels[0].length; 82 | for(let i = 0; i < numSamples; ++i) { 83 | channels[0][i] = this.snd(index++); 84 | channels[1][i] = this.snd(index++); 85 | } 86 | this.sampleIndex = index & 0xffffffff; 87 | this.currentTime += numSamples / 44.1; 88 | } 89 | 90 | return true; 91 | } 92 | } 93 | 94 | registerProcessor('apu', APU); -------------------------------------------------------------------------------- /web/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MicroW8 6 | 7 | 10 | 11 | 12 |
13 | MicroW8 0.4.1 14 |
15 |
16 | 17 | 18 | 19 | 20 |
21 | 22 |
23 | 26 | 27 | 30 | 31 | -------------------------------------------------------------------------------- /web/src/main.js: -------------------------------------------------------------------------------- 1 | import MicroW8 from './microw8.js'; 2 | 3 | function setMessage(size, error) { 4 | let html = size ? `${size} bytes` : 'Insert cart'; 5 | if (error) { 6 | html += ` - ${error.replaceAll('<', '<')}` 7 | } 8 | document.getElementById('message').innerHTML = html; 9 | } 10 | 11 | let uw8 = MicroW8(document.getElementById('screen'), { 12 | setMessage, 13 | keyboardElement: window, 14 | timerElement: document.getElementById("timer"), 15 | startButton: document.getElementById("start") 16 | }); 17 | 18 | function runModuleFromHash() { 19 | let hash = window.location.hash.slice(1); 20 | if(hash == 'devkit') { 21 | uw8.setDevkitMode(true); 22 | return; 23 | } 24 | uw8.setDevkitMode(false); 25 | if (hash.length > 0) { 26 | if (hash.startsWith("url=")) { 27 | uw8.runModuleFromURL(hash.slice(4), true); 28 | } else { 29 | uw8.runModuleFromURL('data:;base64,' + hash); 30 | } 31 | } else { 32 | uw8.runModule(new ArrayBuffer(0)); 33 | } 34 | } 35 | 36 | window.onhashchange = runModuleFromHash; 37 | 38 | let setupLoad = () => { 39 | let loadButton = document.getElementById('cartButton'); 40 | loadButton.style = ''; 41 | loadButton.onclick = () => { 42 | let fileInput = document.createElement('input'); 43 | fileInput.type = 'file'; 44 | fileInput.accept = '.wasm,.uw8,application/wasm'; 45 | fileInput.onchange = () => { 46 | if (fileInput.files.length > 0) { 47 | uw8.runModuleFromURL(URL.createObjectURL(fileInput.files[0])); 48 | } 49 | }; 50 | fileInput.click(); 51 | }; 52 | 53 | screen.ondragover = (e) => { 54 | e.preventDefault(); 55 | }; 56 | 57 | screen.ondrop = (e) => { 58 | let files = e.dataTransfer && e.dataTransfer.files; 59 | if(files && files.length == 1) { 60 | e.preventDefault(); 61 | uw8.runModuleFromURL(URL.createObjectURL(e.dataTransfer.files[0])); 62 | } 63 | } 64 | 65 | runModuleFromHash(); 66 | }; 67 | 68 | let location = window.location; 69 | if(location.hash.length != 0) { 70 | setupLoad(); 71 | } else { 72 | (async () => { 73 | let url = location.href; 74 | if(url.endsWith('.html')) { 75 | url = url.slice(0, url.length - 4) + 'uw8'; 76 | } else { 77 | if(!url.endsWith('/')) { 78 | url += '/'; 79 | } 80 | url += 'cart.uw8'; 81 | } 82 | try { 83 | if(!await uw8.runModuleFromURL(url, true)) { 84 | setupLoad(); 85 | } 86 | } catch(e) { 87 | setupLoad(); 88 | } 89 | })(); 90 | } 91 | 92 | -------------------------------------------------------------------------------- /web/src/run-web.css: -------------------------------------------------------------------------------- 1 | html, body, canvas { 2 | padding: 0; 3 | margin: 0; 4 | background-color: #202024; 5 | } 6 | 7 | html { 8 | height: 100%; 9 | } 10 | 11 | body { 12 | height: 100%; 13 | display: grid; 14 | grid-template-rows: 1fr; 15 | } 16 | 17 | #screen { 18 | align-self: center; 19 | justify-self: center; 20 | image-rendering: pixelated; 21 | border: 4px solid #303040; 22 | } 23 | 24 | #message { 25 | position: absolute; 26 | width: calc(100% - 16px); 27 | background-color: rgba(0, 0, 0, 0.4); 28 | color: #c64; 29 | padding: 8px; 30 | font: bold 12pt sans-serif; 31 | z-index: 2; 32 | } 33 | 34 | @media (min-width: 648px) and (min-height: 488px) { 35 | #screen { 36 | width: 640px; 37 | height: 480px; 38 | } 39 | } 40 | 41 | @media (min-width: 968px) and (min-height: 728px) { 42 | #screen { 43 | width: 960px; 44 | height: 720px; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /web/src/run-web.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | uw8-run 6 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 18 | -------------------------------------------------------------------------------- /web/src/run-web.js: -------------------------------------------------------------------------------- 1 | import MicroW8 from './microw8.js'; 2 | 3 | let uw8 = MicroW8(document.getElementById('screen'), { 4 | setMessage: (_, err) => { 5 | let elem = document.getElementById('message'); 6 | if(err) { 7 | elem.innerText = err; 8 | } 9 | elem.hidden = !err; 10 | } 11 | }); 12 | let events = new EventSource('events'); 13 | events.onmessage = event => { 14 | console.log(event.data); 15 | if(event.data == 'L') { 16 | uw8.runModuleFromURL('cart', true); 17 | } 18 | }; 19 | uw8.runModuleFromURL('cart', true); 20 | -------------------------------------------------------------------------------- /web/src/style.css: -------------------------------------------------------------------------------- 1 | html, body, canvas { 2 | padding: 0; 3 | margin: 0; 4 | } 5 | 6 | html { 7 | font-family: 'Josefin Sans', Verdana, 'Arial Black', sans-serif; 8 | background-color: #202024; 9 | color: #808070; 10 | height: 100%; 11 | } 12 | 13 | body { 14 | height: 100%; 15 | display: grid; 16 | grid-template-rows: 1fr 0fr; 17 | } 18 | 19 | #uw8 { 20 | position: absolute; 21 | } 22 | 23 | #uw8 a { 24 | font-size: 130%; 25 | } 26 | 27 | #centered { 28 | align-self: center; 29 | justify-self: center; 30 | } 31 | 32 | a { 33 | color: #303630; 34 | } 35 | 36 | a:hover { 37 | color: #405040; 38 | } 39 | 40 | .screen { 41 | width: 320px; 42 | height: 240px; 43 | image-rendering: pixelated; 44 | background-color: #202428; 45 | margin-bottom: 8px; 46 | border: 4px solid #303040; 47 | box-shadow: 5px 5px 20px black; 48 | } 49 | 50 | #screen { 51 | cursor: none; 52 | } 53 | 54 | #start { 55 | font-size: 150%; 56 | } 57 | 58 | #timer::before { 59 | content: ''; 60 | display: inline-block; 61 | width: 12px; 62 | height: 12px; 63 | border-radius: 6px; 64 | background-color: red; 65 | margin-right: 3px; 66 | } 67 | 68 | #message { 69 | margin-bottom: 8px; 70 | } 71 | 72 | .error { 73 | color: #e04030; 74 | } 75 | 76 | button { 77 | font-family: inherit; 78 | background-color: #303440; 79 | color: #808070; 80 | padding: 4px; 81 | border: 1px solid #404040; 82 | border-radius: 4px; 83 | } 84 | 85 | button:hover { 86 | background-color: #202010; 87 | } 88 | 89 | button:active { 90 | background-color: #504450; 91 | } 92 | 93 | @media (min-width: 680px) and (min-height: 620px) { 94 | .screen { 95 | width: 640px; 96 | height: 480px; 97 | } 98 | } 99 | 100 | @media (min-width: 1000px) and (min-height: 800px) { 101 | .screen { 102 | width: 960px; 103 | height: 720px; 104 | } 105 | } 106 | 107 | @media (width:640px) and (height:480px) { 108 | .screen { 109 | width: 640px; 110 | height: 480px; 111 | border: 0; 112 | margin: 0; 113 | } 114 | body { 115 | overflow: hidden; 116 | } 117 | } 118 | --------------------------------------------------------------------------------