134 Commits

Author SHA1 Message Date
4e18d38538 add untested implementation of text scale 2024-04-07 23:59:14 +02:00
d4bad3ba8b add sprite flipping to example 2024-04-07 23:20:13 +02:00
6ddda24156 Merge branch 'sprites' 2024-04-07 22:58:26 +02:00
7003db3a05 update dependencies 2024-04-03 10:26:41 +02:00
8258bc1854 fix typo 2023-09-13 07:36:24 +02:00
a651107104 add start and snd functions to filter exports exclude list 2023-09-03 10:19:30 +02:00
22c35e37f4 remove debug trace 2023-09-02 15:37:27 +02:00
805c939097 add support for sound devices that only accept 16bit audio 2023-08-23 23:03:23 +02:00
440e150896 update dependencies 2023-08-23 00:52:51 +02:00
77b2e27346 clamp snd samples into valid -1.0..1.0 range 2023-08-23 00:20:35 +02:00
09e4fcbf14 add --scale option 2023-08-11 23:52:19 +02:00
c8a9f533ef copy optimized blitSprite impl to grabSprite, add simple docs 2023-04-03 23:44:12 +02:00
4a1d607bcb optimize blitSprite 2023-04-03 00:09:02 +02:00
bbb75838cd first unoptimized version of blitSprite & grabSprite 2023-04-02 23:21:31 +02:00
dbeb242fb2 add support for br_table instruction when packing cart 2023-03-20 23:08:42 +01:00
4fe4bce0ad token env var has changed 2023-01-31 00:14:25 +01:00
fe86153562 use fixed version of zola-deploy-action 2023-01-31 00:01:07 +01:00
c9dadaca2e try add safe.directory 2023-01-30 09:48:57 +01:00
5dc3e281ce and again 2023-01-30 09:45:28 +01:00
ce3afb821f another attempt changing the owner to fix permission issue 2023-01-30 09:43:54 +01:00
2652a351ad remove chown again 2023-01-30 09:38:57 +01:00
9109722409 fix main.yml 2023-01-30 09:37:12 +01:00
f861c262a1 update ci actions, hopefully fix permission error 2023-01-30 09:15:09 +01:00
bbfb5eba49 add back event debouncing in file watcher 2023-01-30 00:09:25 +01:00
9e19ff1761 prepare for 0.2.2 release 2023-01-28 12:47:16 +01:00
5d85aeac09 call an exported start function if it exists 2023-01-28 12:31:16 +01:00
30eb953d5d remove test code that was accidentally committed 2023-01-28 11:37:05 +01:00
abdf780533 only open browser once cart has been compiled succesfully 2023-01-28 11:33:49 +01:00
fc05a54e2c fix control codes 4-6 as parameters to other control codes 2023-01-28 10:58:28 +01:00
b33099c828 update more dependencies 2023-01-28 02:11:25 +01:00
d478d3ad49 enable timeapi in winapi crate 2023-01-27 00:13:32 +01:00
502852e59a update uw8-window dependencies 2023-01-26 23:40:54 +01:00
5efa8b3465 first batch of dependency updates 2023-01-26 22:45:34 +01:00
daf2a02cd8 fix sndGes name & add missing auto-import 2023-01-23 23:21:45 +01:00
8d5374a867 add support to ignore empty memory section when packing 2022-11-03 23:27:53 +01:00
142b6a4c15 fix typo in sample rate selection 2022-10-30 00:07:58 +02:00
877fceb089 add --fps parameter to output fps 2022-07-25 23:44:06 +02:00
f0ba0f2b99 update site with 0.2.1 release 2022-07-25 08:46:23 +02:00
760664eb77 add some command line switches for the gpu window 2022-07-23 22:34:59 +02:00
465e66ff4b slight improvement to packed size display 2022-07-23 00:30:14 +02:00
e4579d81bc add chromatic version of fast crt shader + auto crt shader 2022-07-21 23:03:52 +02:00
1f5042059c fix inputs getting stuck 2022-07-21 19:37:36 +02:00
499bb02f2c restructure control flow of uw8-window to hopefully make it work on MacOS 2022-07-21 08:51:17 +02:00
57a92ba79a update version number 2022-07-17 22:44:46 +02:00
7ec1e68a00 very slightly improve frame timings when not quite reaching 60 fps 2022-07-14 00:05:15 +02:00
539d19e0d7 fix square filter, arrow keys on windows 2022-07-12 20:18:06 +02:00
e9a5f702b4 finish fast_crt shader 2022-07-12 00:22:05 +02:00
ba0b037ec2 add first version of fast crt shader 2022-07-11 09:26:11 +02:00
0130d1c906 implement square filter 2022-07-10 23:56:19 +02:00
379ece5cbf restructuring for multiple filters 2022-07-10 23:17:31 +02:00
c9c5cb76bd start refactoring wgpu code to allow for different upscale filters 2022-07-10 16:37:39 +02:00
a6d6615231 only draw area covered by framebuffer 2022-07-10 12:18:53 +02:00
fbc86fa78d implement input for gpu window 2022-07-09 21:16:25 +02:00
eb724e8785 keyboard input is working for cpu window again 2022-07-09 13:18:51 +02:00
f559c5b7d4 restructure run_native to report errors back to caller 2022-07-09 12:24:59 +02:00
9dabf75732 first somewhat working version of uw8 using gpu window 2022-07-08 23:29:39 +02:00
b0adf7748d add uw8-window crate 2022-07-08 22:11:00 +02:00
7aa70ef39d some more tunnel optimization 2022-07-08 22:08:30 +02:00
2ce91ef49c continue TomCat's optimization of the tunnel 2022-06-22 00:18:20 +02:00
7caad08b7c print fractional compressed size 2022-06-22 00:16:35 +02:00
1f6de62e5d add test program for filled circle drawing 2022-06-12 23:54:56 +02:00
caeaa82787 optimize hline function, update wasmtime 2022-06-12 14:28:01 +02:00
e0450c9039 successfully reprod audio recording in firefox 2022-06-09 23:54:43 +02:00
95d0d92a6f first try for a simple repro of firefox resample fail (failed) 2022-06-09 23:36:23 +02:00
7a6dd0ab6d improve formatting of change log 2022-05-09 01:13:20 +02:00
e7a00dd9c6 prepare for v0.2.0 release 2022-05-09 00:51:51 +02:00
a02243d98c add sound to skipahead 2022-05-08 20:28:06 +02:00
599873890a add docs for debug output 2022-05-08 19:47:07 +02:00
8e9bb002bc improve sleep timer resolution on windows 2022-05-08 18:08:29 +02:00
b2b990333e prepare for v0.2.0-rc3 release 2022-05-08 00:51:11 +02:00
d1556f7be8 add support for writing debug output to the console 2022-05-08 00:41:11 +02:00
9f548cd6f0 update curlywas 2022-05-08 00:19:49 +02:00
7cea4eebd3 improve frame timings some more in both runtimes 2022-05-05 09:53:36 +02:00
3f67e92c5c prepare for 0.2.0-rc2 release 2022-05-04 08:48:16 +02:00
a2714f25e4 fix unstable playback in browser runtime 2022-05-04 00:32:41 +02:00
7e203d93e6 implement scheduled sound updates in native runtime 2022-05-02 08:19:34 +02:00
e44c87d1f6 add port of cracklebass 2022-04-29 00:22:55 +02:00
614b7cf358 add 0.2.0-rc1 download links to site 2022-04-26 23:20:05 +02:00
c42a484adb Update version number to 0.2.0-rc1 2022-04-26 22:38:31 +02:00
3a5f2bf865 add another small doc paragraph for snd function 2022-04-26 19:41:05 +02:00
2dee1b30a4 add support for non 44.1kHz audio configs (resampling) 2022-04-26 00:02:34 +02:00
42f7887ab2 install missing alsa dependency on ci 2022-04-24 23:48:58 +02:00
4c82f4ad02 add 0.2.0rc1 web runtime 2022-04-24 23:32:28 +02:00
4dd8c3b029 add audio track to recorded video 2022-04-24 23:17:58 +02:00
2bf8938183 remove back-channel from audio thread for now
It needs some more thought before committing to it.
2022-04-24 15:52:19 +02:00
491bf88ade add first version of sound doku 2022-04-24 00:01:54 +02:00
e05701300c implement backchannel from audio thread 2022-04-22 00:28:19 +02:00
df0c169d54 use the last byte 2022-04-20 23:19:26 +02:00
61941bceeb add simple example for doing music just using playNote function 2022-04-20 21:48:03 +02:00
8fa64519e4 add playNote fn and bell chr (7) 2022-04-19 00:27:53 +02:00
7a52ce4e4c implement custom base address for GES 2022-04-18 16:40:16 +02:00
6f20d303c8 adjust tim_ges to latest sound chip revision 2022-04-18 11:52:07 +02:00
a5edeb21d8 update some dependencies 2022-04-17 22:32:59 +02:00
2839fe5be4 implement sound in native runtime 2022-04-17 22:16:17 +02:00
893158e136 use setTimeout instead of requestAnimationFrame for 60 fps update 2022-04-17 12:26:01 +02:00
7c5f43f152 Merge branch 'master' into sound 2022-04-11 00:23:08 +02:00
f32b0762b0 update curlywas 2022-04-11 00:18:26 +02:00
9ebb6b6d34 pause module when page doesn't have focus 2022-04-10 23:24:04 +02:00
8a10b99eeb fix non-windows build 2022-04-08 21:22:34 +02:00
6c064a1dd8 enable ansi terminal on windows 10 cmd 2022-04-08 21:11:51 +02:00
37f12f5a2c update web runtime 2022-04-04 09:31:43 +02:00
8ad2885a55 implement ring modulation 2022-04-02 18:19:45 +02:00
1917057b81 fixed one pole filter, wide stereo bit 2022-04-02 00:06:04 +02:00
82c1ddb867 improve attack and noise 2022-04-01 00:40:56 +02:00
8713aa8930 adjust tim_ges to new soundchip version 2022-03-19 22:55:40 +01:00
0f82e6e711 removed aliasing in rect and saw oscilators 2022-03-19 14:53:21 +01:00
0ade24ebf6 add source file to build wasm module with just ges emulation 2022-03-19 11:03:05 +01:00
29186c806f add pulse width support to other wave types 2022-03-10 23:05:07 +01:00
b626d2609a implement proper exponential envelope timings 2022-03-09 23:03:15 +01:00
39ead8220f slight optimization, add pulse width modulation to melody voices 2022-03-09 09:22:27 +01:00
ce18a8a162 add initial pulse width support 2022-03-08 22:52:12 +01:00
a15e796489 first full version of time for ges 2022-03-08 22:20:54 +01:00
f178076b86 all basic wave forms, filters, panning 2022-03-08 09:46:35 +01:00
81adcf0198 implement more of the sound-chip 2022-03-07 23:58:54 +01:00
780caf965a sync sound registers to sound thread 2022-03-07 09:35:11 +01:00
2033f9a172 wait for audio ready before starting cart, add button to unsuspend audio
fixes missing sound when auto-starting cart in chrome
2022-03-06 14:08:44 +01:00
0d514c7dd3 steady on now down to 197 bytes 2022-03-06 10:13:48 +01:00
a8eb3bda27 some clean up and optimization on steady on tim 2022-03-05 23:54:13 +01:00
8b765a5742 Merge branch 'master' into sound 2022-03-05 23:10:14 +01:00
7197c11586 fix microw8.html no-autoload mode 2022-03-05 23:10:04 +01:00
99a423619e fix watch mode not working when initial compile fails 2022-03-05 21:18:55 +01:00
9063e872d3 ported steady on tim as a sound test 2022-03-04 23:38:27 +01:00
85240599e8 first working version with sound 2022-03-04 09:50:10 +01:00
35ec5fdb59 add simple bytebeat example to test first implementation with 2022-03-02 22:42:23 +01:00
a6a82ff5a1 update wasmtime version 2022-03-02 21:50:26 +01:00
973814a629 update hero link to 0.1.2 2022-03-02 08:54:34 +01:00
00d21b4745 add include dir to release script 2022-02-28 23:59:51 +01:00
d11b46576a prepare for 0.1.2 release 2022-02-28 23:19:58 +01:00
add49a1f8b fix #2: Crash when drawing zero sized line 2022-02-27 22:09:54 +01:00
8815a8e02e add full dependencies to file watcher 2022-02-27 21:50:33 +01:00
eb7c33d412 update curlywas, use 'include "microw8-api.cwa"' in examples 2022-02-26 23:41:15 +01:00
47ad3b4f30 add command to uw8-tool to write api include file 2022-02-26 22:13:15 +01:00
0f668fb6e9 fix typo in atan2 docs, change 'i' character in font 2022-02-26 21:01:57 +01:00
f876f59e80 Merge branch 'browser-run' into next 2022-02-26 21:00:58 +01:00
88 changed files with 10713 additions and 5661 deletions

View File

@@ -27,12 +27,12 @@ jobs:
steps: steps:
- name: Install dependencies - name: Install dependencies
run: sudo apt-get install -y libxkbcommon-dev run: sudo apt-get install -y libxkbcommon-dev libasound2-dev
if: matrix.os == 'ubuntu-latest' if: matrix.os == 'ubuntu-latest'
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Cache build dirs - name: Cache build dirs
uses: actions/cache@v2 uses: actions/cache@v3
with: with:
path: | path: |
~/.cargo/bin/ ~/.cargo/bin/
@@ -44,7 +44,7 @@ jobs:
- name: Build - name: Build
run: cargo build --release --verbose run: cargo build --release --verbose
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
with: with:
name: uw8-${{ matrix.build }} name: uw8-${{ matrix.build }}
path: target/release/${{ matrix.exe }} path: target/release/${{ matrix.exe }}

View File

@@ -8,12 +8,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: build_and_deploy - name: build_and_deploy
uses: shalzz/zola-deploy-action@v0.14.1 uses: shalzz/zola-deploy-action@70a101a14bbdeed13e7a42a9ed06b35c9e9e826e
env: env:
# Target branch # Target branch
PAGES_BRANCH: gh-pages PAGES_BRANCH: gh-pages
BUILD_DIR: site BUILD_DIR: site
# Provide personal access token # Provide personal access token
TOKEN: $GITHUB_ACTOR:${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

3889
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,31 @@
[package] [package]
name = "uw8" name = "uw8"
version = "0.1.1" version = "0.2.2"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features] [features]
default = ["native", "browser"] default = ["native", "browser"]
native = ["wasmtime"] native = ["wasmtime", "uw8-window", "cpal", "rubato" ]
browser = ["warp", "tokio", "tokio-stream", "webbrowser"] browser = ["warp", "tokio", "tokio-stream", "webbrowser"]
[dependencies] [dependencies]
wasmtime = { version = "0.30", optional = true } wasmtime = { version = "19.0.1", optional = true }
anyhow = "1" anyhow = "1"
minifb = { version = "0.20", default-features = false, features = ["x11"] } env_logger = "0.11.3"
notify = "4" log = "0.4"
pico-args = "0.4" uw8-window = { path = "uw8-window", optional = true }
curlywas = { git = "https://github.com/exoticorn/curlywas.git", rev = "196719b" } notify-debouncer-mini = { version = "0.4.1", default-features = false }
pico-args = "0.5"
curlywas = { git = "https://github.com/exoticorn/curlywas.git", rev = "0e7ea50" }
wat = "1" wat = "1"
uw8-tool = { path = "uw8-tool" } uw8-tool = { path = "uw8-tool" }
same-file = "1" same-file = "1"
warp = { version = "0.3.2", optional = true } warp = { version = "0.3.6", optional = true }
tokio = { version = "1.17.0", features = ["sync", "rt"], optional = true } tokio = { version = "1.37.0", features = ["sync", "rt"], optional = true }
tokio-stream = { version = "0.1.8", features = ["sync"], optional = true } tokio-stream = { version = "0.1.15", features = ["sync"], optional = true }
webbrowser = { version = "0.6.0", optional = true } webbrowser = { version = "0.8.13", optional = true }
ansi_term = "0.12.1"
cpal = { version = "0.15.3", optional = true }
rubato = { version = "0.15.0", optional = true }

View File

@@ -15,9 +15,9 @@ See [here](https://exoticorn.github.io/microw8/) for more information and docs.
## Downloads ## Downloads
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.1.0/microw8-0.1.0-linux.tgz) * [Linux](https://github.com/exoticorn/microw8/releases/download/v0.2.2/microw8-0.2.2-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.1.0/microw8-0.1.0-macos.tgz) * [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.2.2/microw8-0.2.2-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.1.0/microw8-0.1.0-windows.zip) * [Windows](https://github.com/exoticorn/microw8/releases/download/v0.2.2/microw8-0.2.2-windows.zip)
The download includes The download includes
@@ -35,6 +35,7 @@ Runs <file> which can be a binary WebAssembly module, an `.uw8` cart, a wat (Web
Options: Options:
-b, --browser : Run in browser instead of using native runtime
-t, --timeout FRAMES : Sets the timeout in frames (1/60s) -t, --timeout FRAMES : Sets the timeout in frames (1/60s)
-w, --watch : Reloads the given file every time it changes on disk. -w, --watch : Reloads the given file every time it changes on disk.
-p, --pack : Pack the file into an .uw8 cart before running it and print the resulting size. -p, --pack : Pack the file into an .uw8 cart before running it and print the resulting size.
@@ -42,6 +43,27 @@ Options:
-l LEVEL, --level LEVEL : Compression level (0-9). Higher compression levels are really slow. -l LEVEL, --level LEVEL : Compression level (0-9). Higher compression levels are really slow.
-o FILE, --output FILE : Write the loaded and optionally packed cart back to disk. -o FILE, --output FILE : Write the loaded and optionally packed cart back to disk.
when using the native runtime:
-m, --no-audio : Disable audio, also reduces cpu load a bit
--no-gpu : Force old cpu-only window code
--filter FILTER : Select an upscale filter at startup
--fullscreen : Start in fullscreen mode
Note that the cpu-only window does not support fullscreen nor upscale filters.
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.
Therefore you should rarely need to manually pass --no-gpu. If you prefer the old pixel doubling look to the now default crt filter,
you can just pass "--filter nearest" or "--filter 1".
The upscale filter options are:
1, nearest : Anti-aliased nearest filter
2, fast_crt : Very simple, cheap crt filter, not very good below a window size of 960x720
3, ss_crt : Super sampled crt filter, a little more demanding on the GPU but scales well to smaller window sizes
4, chromatic_crt : Variant of fast_crt with a slight offset of the three color dots of a pixel, still pretty cheap
5, auto_crt (default) : ss_crt below 960x720, chromatic_crt otherwise
You can switch the upscale filter at any time using the keys 1-5. You can toggle fullscreen with F.
uw8 pack [<options>] <infile> <outfile> uw8 pack [<options>] <infile> <outfile>

View File

@@ -1,5 +1,4 @@
import "env.memory" memory(4); include "../include/microw8-api.cwa"
import "env.printString" fn printString(i32);
export fn upd() { export fn upd() {
printString(0x20000); printString(0x20000);

View File

@@ -0,0 +1,55 @@
// port of cracklebass by pestis (originally on TIC-80)
include "../include/microw8-api.cwa"
const MUSIC_DATA = 0x20000;
export fn upd() {
let inline t = 32!32 * 6 / 100;
let inline p = t / 1024;
let channel:i32;
loop channels {
let inline e = t * channel?MUSIC_DATA / 8;
let lazy pattern = (8 * channel + p)?(MUSIC_DATA + 56);
let lazy n = !!pattern * (8 * pattern + e / 16 % 8)?MUSIC_DATA;
let inline prev_ctrl = (channel * 6)?80;
(channel * 6)?80 = if n {
let inline base_note = 12 + 12 * channel?(MUSIC_DATA + 4) + n;
let inline pitch_drop = e % 16 * channel?(MUSIC_DATA + 94);
let inline key_pattern = p?(MUSIC_DATA + 8*4 + 56);
let inline key = select(key_pattern, (8 * key_pattern + t / 128 % 8)?MUSIC_DATA, 1);
(channel * 6)?83 = base_note - pitch_drop / 4 + key;
prev_ctrl & 0xfc | (e / 8 & 2) | 1
} else {
prev_ctrl & 0xfe
};
branch_if (channel := channel + 1) < 4: channels;
}
}
data 80 {
i8(
0x44, 0, 0, 0, 0x50, 0x40,
0x4, 0x50, 0, 0, 0x80, 0x80,
0x40, 0x80, 0, 0, 0x40, 0x40,
0, 0, 0, 0, 0x50, 0x50
)
}
data MUSIC_DATA {
i8(
16, 2, 8, 8, 1, 2, 2, 3, 1, 0,
1,13,16, 0, 1, 8, 1, 0, 1,13,
16, 1, 1, 8, 1, 0, 8,13,13, 0,
16,13, 1, 0, 1, 0, 1, 0, 1, 1,
1, 0, 0, 0, 1, 0,13, 1, 1, 1,
6, 8, 1, 1, 6, 8, 1, 1, 2, 1,
2, 1, 2, 0, 0, 0, 0, 3, 3, 3,
5, 0, 0, 2, 1, 2, 1, 2, 1, 2,
0, 4, 4, 0, 4, 4, 4, 4, 0, 0,
0, 0, 6, 6, 0, 0, 0, 8
)
}

View File

@@ -1,11 +1,4 @@
import "env.time" fn time() -> f32; include "../include/microw8-api.cwa"
import "env.circle" fn circle(f32, f32, f32, i32);
import "env.cls" fn cls(i32);
import "env.randomSeed" fn seed(i32);
import "env.randomf" fn randomf() -> f32;
import "env.sin" fn sin(f32) -> f32;
import "env.cos" fn cos(f32) -> f32;
import "env.fmod" fn fmod(f32, f32) -> f32;
export fn upd() { export fn upd() {
cls(0); cls(0);
@@ -15,10 +8,10 @@ export fn upd() {
let inline rocket = i #>> 9; let inline rocket = i #>> 9;
let lazy local_time = fmod(time() + rocket as f32 / 5 as f32, 2 as f32); let lazy local_time = fmod(time() + rocket as f32 / 5 as f32, 2 as f32);
let lazy rocket = rocket + nearest(time() - local_time) as i32 * 10; let lazy rocket = rocket + nearest(time() - local_time) as i32 * 10;
seed(rocket); randomSeed(rocket);
let inline x = randomf() * 645 as f32; let inline x = randomf() * 645 as f32;
let y = randomf() * 133 as f32; let y = randomf() * 133 as f32;
let lazy angle = { seed(i); randomf() } * 44 as f32; let lazy angle = { randomSeed(i); randomf() } * 44 as f32;
let inline dx = sin(angle); let inline dx = sin(angle);
let inline dy = cos(angle); let inline dy = cos(angle);
let lazy dist = local_time * (randomf() * 44 as f32); let lazy dist = local_time * (randomf() * 44 as f32);

View File

@@ -1,38 +1,30 @@
import "env.memory" memory(4); include "../include/microw8-api.cwa"
import "env.cls" fn cls(i32);
import "env.printString" fn printString(i32);
import "env.printChar" fn printChar(i32);
import "env.setCursorPosition" fn setCursor(i32, i32);
import "env.setTextColor" fn setTextColor(i32);
import "env.line" fn line(f32, f32, f32, f32, i32);
import "env.isButtonTriggered" fn triggered(i32) -> i32;
global mut mode: i32 = 0; global mut mode: i32 = 0;
export fn upd() { export fn upd() {
cls(0); cls(0);
if triggered(4) { if isButtonTriggered(BUTTON_A) {
mode = !mode; mode = !mode;
} }
setTextColor(15); setTextColor(15);
printString(mode * 0x20000); printString(mode * USER_MEM);
let y: i32; let y: i32;
loop y { loop y {
line(0 as f32, (y * 9 + 39) as f32, (14+16*9) as f32, (y * 9 + 39) as f32, 1); line(0 as f32, (y * 9 + 39) as f32, (14+16*9) as f32, (y * 9 + 39) as f32, 1);
line((y * 9 + 15) as f32, 24 as f32, (y * 9 + 15) as f32, (38+16*9) as f32, 1); line((y * 9 + 15) as f32, 24 as f32, (y * 9 + 15) as f32, (38+16*9) as f32, 1);
setTextColor(15); setTextColor(15);
setCursor(y * 9 + 16, 24); setCursorPosition(y * 9 + 16, 24);
let lazy hexChar = select(y < 10, y + 48, y + 87); let lazy hexChar = select(y < 10, y + 48, y + 87);
printChar(hexChar); printChar(hexChar);
setCursor(0, y * 9 + 24+16); setCursorPosition(0, y * 9 + 24+16);
printChar(hexChar); printChar(hexChar);
let x = 0; let x = 0;
loop x { loop x {
setCursor(x * 9 + 16, y * 9 + 24+16); setCursorPosition(x * 9 + 16, y * 9 + 24+16);
setTextColor(select(mode, x + y * 16, -9)); setTextColor(select(mode, x + y * 16, -9));
if y >= 2 | mode { if y >= 2 | mode {
printChar(select(mode, 0xa4, x + y * 16)); printChar(select(mode, 0xa4, x + y * 16));
@@ -47,6 +39,6 @@ data 0 {
"Default font: (press " i8(0xcc) " for palette)" i8(5, 0) "Default font: (press " i8(0xcc) " for palette)" i8(5, 0)
} }
data 0x20000 { data USER_MEM {
"Default palette: (press " i8(0xcc) " for font)" i8(5, 0) "Default palette: (press " i8(0xcc) " for font)" i8(5, 0)
} }

View File

@@ -1,10 +1,4 @@
import "env.memory" memory(4); include "../include/microw8-api.cwa"
import "env.cls" fn cls(i32);
import "env.time" fn time() -> f32;
import "env.line" fn line(f32, f32, f32, f32, i32);
import "env.sin" fn sin(f32) -> f32;
import "env.cos" fn cos(f32) -> f32;
export fn upd() { export fn upd() {
cls(0); cls(0);

View File

@@ -0,0 +1,38 @@
include "../include/microw8-api.cwa"
global mut frame = 0;
export fn upd() {
if frame % 16 == 0 {
let ch: i32;
loop channels {
playNote(ch, (ch * 32 + (frame / 16) % 32)?0x20000);
branch_if ch := (ch + 1) % 4: channels;
}
}
frame = frame + 1;
}
data 0x20000 {
i8(
0x4e, 0x0, 0x0, 0x4c, 0x49, 0x0, 0x45, 0x47,
0x49, 0x47, 0x45, 0x44, 0x42, 0x0, 0x3d, 0x41,
0x44, 0x0, 0x0, 0x47, 0x49, 0x47, 0x45, 0x41,
0x44, 0x0, 0x0, 0x0, 0x42, 0x0, 0x0, 0x0,
0x25, 0, 0x49, 0x25, 0x25, 0, 0x49, 0x38,
0x25, 0, 0x49, 0x25, 0x25, 0, 0x49, 0x38,
0x25, 0, 0x49, 0x25, 0x25, 0, 0x49, 0x38,
0x25, 0, 0x49, 0x25, 0x25, 0, 0x49, 0x38,
0x2a, 0x0, 0x0, 0x0, 0x2d, 0x0, 0x0, 0x0,
0x2c, 0x0, 0x28, 0x0, 0x2a, 0x0, 0x0, 0x0,
0x25, 0x0, 0x0, 0x0, 0x29, 0x0, 0x0, 0x0,
0x2c, 0x0, 0x2d, 0x0, 0x2a, 0x0, 0x25, 0x0,
0x0, 0x0, 0x31, 0x0, 0x34, 0x0, 0x0, 0x36,
0x38, 0x39, 0x38, 0x34, 0x36, 0x0, 0x0, 0x0,
0x0, 0x3d, 0x3b, 0x39, 0x38, 0x0, 0x0, 0x0,
0x0, 0x39, 0x38, 0x39, 0x38, 0x0, 0x36, 0x0
)
}

View File

@@ -1,12 +1,4 @@
import "env.memory" memory(4); include "../include/microw8-api.cwa"
import "env.rectangle" fn rect(f32, f32, f32, f32, i32);
import "env.circle" fn circle(f32, f32, f32, i32);
import "env.isButtonPressed" fn btn(i32) -> i32;
import "env.random" fn random() -> i32;
import "env.randomSeed" fn randomSeed(i32);
import "env.cls" fn cls(i32);
import "env.printInt" fn printInt(i32);
global mut pz: i32 = 4; global mut pz: i32 = 4;
global mut px: f32 = 2.0; global mut px: f32 = 2.0;
@@ -16,10 +8,10 @@ global mut f: f32 = 2.0;
export fn upd() { export fn upd() {
let y: i32; let y: i32;
let inline zero = 0.0; let inline zero = 0_f;
let lazy control_speed = 0.03125; let lazy control_speed = 0.03125;
s = s + 0.1875 - (f + control_speed) * btn(4 <| cls(4)) as f32; s = s + 0.1875 - (f + control_speed) * isButtonPressed(4 <| cls(4)) as f32;
f = f * 0.5625; f = f * 0.5625;
printInt(pz); printInt(pz);
@@ -33,11 +25,13 @@ export fn upd() {
let inline c = (z & 1) * -2; let inline c = (z & 1) * -2;
let inline yf = y as f32; let inline yf = y as f32;
rect(rx, yf, rw, yf / 6 as f32, c + 1); rectangle(rx, yf, rw, yf / 6 as f32, c + 1);
rect(rx, yf, rw, 1 as f32, c - 4); rectangle(rx, yf, rw, 1 as f32, c - 4);
if y == 180 & py > zero { if y == 180 & py > zero {
if x > w | x < zero { if x > w | x < zero {
0?80 = 0xc3;
3?80 = 32;
return; return;
} }
py = zero; py = zero;
@@ -51,7 +45,10 @@ export fn upd() {
circle(160 as f32, 160 as f32 + py, 22 as f32, -28); circle(160 as f32, 160 as f32 + py, 22 as f32, -28);
circle((160 - 6) as f32, (160 - 6) as f32 + py, 6 as f32, -26); circle((160 - 6) as f32, (160 - 6) as f32 + py, 6 as f32, -26);
px = px + (btn(3) - btn(2)) as f32 * control_speed; 0?86 = py < zero;
3?86 = 32 - py as i32;
px = px + (isButtonPressed(3) - isButtonPressed(2)) as f32 * control_speed;
py = py + s; py = py + s;
pz = pz + 1; pz = pz + 1;
} }

View File

@@ -0,0 +1,21 @@
include "../include/microw8-api.cwa"
const SPRITE = 0x20000;
export fn upd() {
cls(0);
let t = time() / 2_f;
let i: i32;
loop spriteLoop {
let inline x = sin(t * -1.3 + i as f32 * (3.141 / 30_f)) * 180_f + 160_f;
let inline y = sin(t * 1.7 + i as f32 * (3.141 / 40_f)) * 140_f + 120_f;
blitSprite(SPRITE, 16, x as i32, y as i32, (i & 3) * 0x200 + 0x100);
branch_if (i +:= 1) < 100: spriteLoop;
}
}
start fn start() {
printChar('OO');
circle(8_f, 8_f, 6_f, 75);
grabSprite(SPRITE, 16, 0, 0, 0);
}

View File

@@ -0,0 +1,38 @@
// Steady On Tim, It's Only A Budget Game
// by Gasman / Hooy-Program
// ported to MicroW8 by exoticorn/icebird
include "../include/microw8-api.cwa"
fn melody(t: i32, T: i32) -> i32 {
let inline riff_pos = abs(((T&31) - 16) as f32) as i32;
let lazy shift = ((1-((T>>5)&3))%2-1) as f32 / 6 as f32;
let inline note_count = 5 - (T >= 512);
let inline octave = (riff_pos/5) as f32;
let inline riff_note = 5514 >> (riff_pos % note_count * 4) & 15;
let inline melody_freq = pow(2 as f32, shift + octave - (riff_note as f32 / 12 as f32));
let inline melody = (t as f32 * melody_freq) as i32 & 128;
let inline arp_note = ((0x85>>((t>>12)%3*4)) & 15) - 1;
let inline arp_freq = pow(2 as f32, shift + (arp_note as f32 / 12 as f32));
let inline arp_vol = (T >= 256) * (12-T%12);
let inline arpeggio = ((t as f32 * arp_freq) as i32 & 128) * arp_vol / 12;
melody + arpeggio
}
export fn snd(t: i32) -> f32 {
let lazy T = t/10000;
let inline mel_arp = melody(t, T)/3 + melody(t, T-3)/5;
let inline bass_vol = (T >= 128) & (197 >> (T % 8));
let inline bass_freq = pow(2 as f32, (((T & 4) * ((T & 7) - 1)) as f32 / 24 as f32 - 5 as f32));
let inline bass = ((t as f32 * bass_freq) as i32 & 63) * bass_vol;
let inline snare_ish = (random() & 31) * (8 - (T + 4) % 8) / 8;
let inline sample = mel_arp + bass + snare_ish;
sample as f32 / 255 as f32
}

View File

@@ -1,7 +1,4 @@
import "env.memory" memory(4); include "../include/microw8-api.cwa"
import "env.sin" fn sin(f32) -> f32;
import "env.time" fn time() -> f32;
import "env.setPixel" fn setPixel(i32, i32, i32);
export fn upd() { export fn upd() {
let x: i32; let x: i32;

View File

@@ -0,0 +1,64 @@
// Steady On Tim, It's Only A Budget Game
// original bytebeat by Gasman / Hooy-Program
// ported to MicroW8/GES by exoticorn/icebird
import "env.memory" memory(4);
fn melody(ch: i32, t: i32, T: i32) {
let lazy riff_pos = abs(((T&31) - 16) as f32) as i32;
let lazy shift = ((1-((T>>5)&3))%2-1) * 2;
let inline note_count = 5 - (T >= 512);
let inline octave = (riff_pos/5) * 12;
let inline riff_note = 5514 >> (riff_pos % note_count * 4) & 15;
let inline melody_note = shift + octave - riff_note;
ch?1 = 230 - riff_pos * 14;
ch?3 = melody_note + 64;
let inline arp_note = shift + ((0x85>>((t/2)%3*4)) & 15) - 1;
80?3 = arp_note + 64;
}
export fn upd() {
let lazy t = 32!32 / (1000/60);
let lazy T = t / 7;
melody(98, t, T - 3);
melody(92, t, T);
80?0 = ((T >= 256) & (T/12+(T-3)/12)) * 2 | 0x48; // arp trigger
if T >= 128 {
let inline bass_step = T % 8;
86?3 = if bass_step / 2 == 2 {
86?0 = 0xd6;
81
} else {
86?0 = ((197 >> bass_step) & 1) | 0x48;
((T & 4) * ((T & 7) - 1)) / 2 + 28
};
}
}
data 80 {
i8(
0, 0x90, 0, 0, 0, 0x90,
0, 0x4c, 0, 0, 0, 0x4c,
0x19, 0, 0, 0, 0, 0x4c,
0x19, 0, 0, 0, 0, 0x4c,
0xfa, 0x84,
0xc1, 0xc1, 0, 107, 0, 0x4c
)
}
/*
include "../../platform/src/ges.cwa"
import "env.pow" fn pow(f32, f32) -> f32;
import "env.exp" fn exp(f32) -> f32;
import "env.sin" fn sin(f32) -> f32;
export fn snd(t: i32) -> f32 {
gesSnd(t)
}
*/

View File

@@ -1,7 +1,4 @@
import "env.memory" memory(2); include "../include/microw8-api.cwa"
import "env.fmod" fn fmod(f32, f32) -> f32;
import "env.time" fn time() -> f32;
export fn upd() { export fn upd() {
let i: i32; let i: i32;

View File

@@ -1,7 +1,4 @@
import "env.memory" memory(4); include "../include/microw8-api.cwa"
import "env.atan2" fn atan2(f32, f32) -> f32;
import "env.time" fn time() -> f32;
export fn upd() { export fn upd() {
let i: i32; let i: i32;
@@ -12,7 +9,7 @@ export fn upd() {
let inline d = 40000 as f32 / sqrt(x * x + y * y); let inline d = 40000 as f32 / sqrt(x * x + y * y);
let inline u = atan2(x, y) * (512.0 / 3.141); let inline u = atan2(x, y) * (512.0 / 3.141);
let inline c = ((i32.trunc_sat_f32_s(d + t * 2 as f32) ^ i32.trunc_sat_f32_s(u + t)) & 255) >> 4; let inline c = ((i32.trunc_sat_f32_s(d + t * 2 as f32) ^ i32.trunc_sat_f32_s(u + t)) & 255) >> 4;
i?120 = c; i?FRAMEBUFFER = c;
branch_if (i := i + 1) < 320*240: pixels; branch_if (i := i + 1) < 320*240: pixels;
} }

View File

@@ -0,0 +1,54 @@
// MicroW8 APIs, to be `include`d in CurlyWas sources
import "env.memory" memory(4);
import "env.sin" fn sin(f32) -> f32;
import "env.cos" fn cos(f32) -> f32;
import "env.tan" fn tan(f32) -> f32;
import "env.asin" fn asin(f32) -> f32;
import "env.acos" fn acos(f32) -> f32;
import "env.atan" fn atan(f32) -> f32;
import "env.atan2" fn atan2(f32, f32) -> f32;
import "env.pow" fn pow(f32, f32) -> f32;
import "env.log" fn log(f32) -> f32;
import "env.fmod" fn fmod(f32, f32) -> f32;
import "env.random" fn random() -> i32;
import "env.randomf" fn randomf() -> f32;
import "env.randomSeed" fn randomSeed(i32);
import "env.cls" fn cls(i32);
import "env.setPixel" fn setPixel(i32, i32, i32);
import "env.getPixel" fn getPixel(i32, i32) -> i32;
import "env.hline" fn hline(i32, i32, i32, i32);
import "env.rectangle" fn rectangle(f32, f32, f32, f32, i32);
import "env.circle" fn circle(f32, f32, f32, i32);
import "env.line" fn line(f32, f32, f32, f32, i32);
import "env.time" fn time() -> f32;
import "env.isButtonPressed" fn isButtonPressed(i32) -> i32;
import "env.isButtonTriggered" fn isButtonTriggered(i32) -> i32;
import "env.printChar" fn printChar(i32);
import "env.printString" fn printString(i32);
import "env.printInt" fn printInt(i32);
import "env.setTextColor" fn setTextColor(i32);
import "env.setBackgroundColor" fn setBackgroundColor(i32);
import "env.setCursorPosition" fn setCursorPosition(i32, i32);
import "env.rectangleOutline" fn rectangleOutline(f32, f32, f32, f32, i32);
import "env.circleOutline" fn circleOutline(f32, f32, f32, i32);
import "env.exp" fn exp(f32) -> f32;
import "env.playNote" fn playNote(i32, i32);
import "env.sndGes" fn sndGes(i32) -> f32;
import "env.blitSprite" fn blitSprite(i32, i32, i32, i32, i32);
import "env.grabSprite" fn grabSprite(i32, i32, i32, i32, i32);
const TIME_MS = 0x40;
const GAMEPAD = 0x44;
const FRAMEBUFFER = 0x78;
const PALETTE = 0x13000;
const FONT = 0x13400;
const USER_MEM = 0x14000;
const BUTTON_UP = 0x0;
const BUTTON_DOWN = 0x1;
const BUTTON_LEFT = 0x2;
const BUTTON_RIGHT = 0x3;
const BUTTON_A = 0x4;
const BUTTON_B = 0x5;
const BUTTON_X = 0x6;
const BUTTON_Y = 0x7;

View File

@@ -0,0 +1,56 @@
;; MicroW8 APIs, in WAT (Wasm Text) format
(import "env" "memory" (memory 4))
(import "env" "sin" (func $sin (param f32) (result f32)))
(import "env" "cos" (func $cos (param f32) (result f32)))
(import "env" "tan" (func $tan (param f32) (result f32)))
(import "env" "asin" (func $asin (param f32) (result f32)))
(import "env" "acos" (func $acos (param f32) (result f32)))
(import "env" "atan" (func $atan (param f32) (result f32)))
(import "env" "atan2" (func $atan2 (param f32) (param f32) (result f32)))
(import "env" "pow" (func $pow (param f32) (param f32) (result f32)))
(import "env" "log" (func $log (param f32) (result f32)))
(import "env" "fmod" (func $fmod (param f32) (param f32) (result f32)))
(import "env" "random" (func $random (result i32)))
(import "env" "randomf" (func $randomf (result f32)))
(import "env" "randomSeed" (func $randomSeed (param i32)))
(import "env" "cls" (func $cls (param i32)))
(import "env" "setPixel" (func $setPixel (param i32) (param i32) (param i32)))
(import "env" "getPixel" (func $getPixel (param i32) (param i32) (result i32)))
(import "env" "hline" (func $hline (param i32) (param i32) (param i32) (param i32)))
(import "env" "rectangle" (func $rectangle (param f32) (param f32) (param f32) (param f32) (param i32)))
(import "env" "circle" (func $circle (param f32) (param f32) (param f32) (param i32)))
(import "env" "line" (func $line (param f32) (param f32) (param f32) (param f32) (param i32)))
(import "env" "time" (func $time (result f32)))
(import "env" "isButtonPressed" (func $isButtonPressed (param i32) (result i32)))
(import "env" "isButtonTriggered" (func $isButtonTriggered (param i32) (result i32)))
(import "env" "printChar" (func $printChar (param i32)))
(import "env" "printString" (func $printString (param i32)))
(import "env" "printInt" (func $printInt (param i32)))
(import "env" "setTextColor" (func $setTextColor (param i32)))
(import "env" "setBackgroundColor" (func $setBackgroundColor (param i32)))
(import "env" "setCursorPosition" (func $setCursorPosition (param i32) (param i32)))
(import "env" "rectangleOutline" (func $rectangleOutline (param f32) (param f32) (param f32) (param f32) (param i32)))
(import "env" "circleOutline" (func $circleOutline (param f32) (param f32) (param f32) (param i32)))
(import "env" "exp" (func $exp (param f32) (result f32)))
(import "env" "playNote" (func $playNote (param i32) (param i32)))
(import "env" "sndGes" (func $sndGes (param i32) (result f32)))
(import "env" "blitSprite" (func $blitSprite (param i32) (param i32) (param i32) (param i32) (param i32)))
(import "env" "grabSprite" (func $grabSprite (param i32) (param i32) (param i32) (param i32) (param i32)))
;; to use defines, include this file with a preprocessor
;; like gpp (https://logological.org/gpp).
#define TIME_MS 0x40;
#define GAMEPAD 0x44;
#define FRAMEBUFFER 0x78;
#define PALETTE 0x13000;
#define FONT 0x13400;
#define USER_MEM 0x14000;
#define BUTTON_UP 0x0;
#define BUTTON_DOWN 0x1;
#define BUTTON_LEFT 0x2;
#define BUTTON_RIGHT 0x3;
#define BUTTON_A 0x4;
#define BUTTON_B 0x5;
#define BUTTON_X 0x6;
#define BUTTON_Y 0x7;

View File

@@ -0,0 +1,74 @@
(module
(import "env" "atan2" (func $atan2 (param f32 f32) (result f32)))
(import "env" "time" (func $time (result f32)))
(import "env" "memory" (memory 4))
(func (export "upd")
(local $y i32)
(local $i i32)
(local $x i32)
(loop $pixels
i32.const 1
local.get $i
local.get $i
i32.const 36928
f32.convert_i32_s
local.get $i
i32.const 320
i32.rem_s
i32.const 160
i32.sub
local.tee $x
local.get $x
i32.mul
local.get $i
i32.const 320
i32.div_s
i32.const 120
i32.sub
local.tee $y
local.get $y
i32.mul
i32.add
f32.convert_i32_s
f32.sqrt
f32.div
call $time
i32.const 163
f32.convert_i32_s
f32.mul
f32.add
i32.trunc_sat_f32_s
local.get $x
f32.convert_i32_s
local.get $y
f32.convert_i32_s
call $atan2
i32.const 163
f32.convert_i32_s
f32.mul
call $time
i32.const 64
f32.convert_i32_s
f32.mul
f32.add
i32.trunc_f32_s
i32.xor
i32.const 4
i32.shr_s
i32.const 15
i32.and
i32.store8 offset=120
i32.add
local.tee $i
i32.const 76800
i32.rem_s
br_if $pixels
)
)
)

301
platform/Cargo.lock generated
View File

@@ -79,9 +79,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "chumsky" name = "chumsky"
version = "0.5.0" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2d3efff85e8572b1c3fa0127706af58c4fff8458f8d9436d54b1e97573c7a3f" checksum = "8d02796e4586c6c41aeb68eae9bfb4558a522c35f1430c14b40136c3706e09e4"
dependencies = [ dependencies = [
"ahash 0.3.8", "ahash 0.3.8",
] ]
@@ -110,9 +110,9 @@ dependencies = [
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.3.0" version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "738c290dfaea84fc1ca15ad9c168d083b05a714e1efddd8edaab678dc28d2836" checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
] ]
@@ -146,37 +146,44 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]] [[package]]
name = "curlywas" name = "curlywas"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/exoticorn/curlywas.git?rev=196719b#196719b35ef377cb7e001554b27ac5de013dcf2b" source = "git+https://github.com/exoticorn/curlywas.git?rev=0e7ea50#0e7ea508cd0e76836283ae68a44c9097df83c8ac"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"ariadne", "ariadne",
"chumsky", "chumsky",
"pico-args", "pico-args 0.4.2",
"wasm-encoder", "wasm-encoder 0.10.0",
"wasmparser", "wasmparser 0.83.0",
] ]
[[package]] [[package]]
name = "fallible_collections" name = "fallible_collections"
version = "0.4.4" version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52db5973b6a19247baf19b30f41c23a1bfffc2e9ce0a5db2f60e3cd5dc8895f7" checksum = "3f57ccc32870366ae684be48b32a1a2e196f98a42a9b4361fe77e13fd4a34755"
dependencies = [ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.22" version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
dependencies = [ dependencies = [
"cfg-if",
"crc32fast", "crc32fast",
"libc",
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "form_urlencoded"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
dependencies = [
"percent-encoding",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.3" version = "0.2.3"
@@ -190,13 +197,48 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.11.2" version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [ dependencies = [
"ahash 0.7.6", "ahash 0.7.6",
] ]
[[package]]
name = "heck"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "id-arena"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005"
[[package]]
name = "idna"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
dependencies = [
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "indexmap"
version = "1.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.4.0" version = "1.4.0"
@@ -210,17 +252,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
[[package]] [[package]]
name = "libc" name = "lexopt"
version = "0.2.112" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" checksum = "478ee9e62aaeaf5b140bd4138753d1f109765488581444218d3ddda43234f3e8"
[[package]]
name = "libc"
version = "0.2.139"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
[[package]] [[package]]
name = "lodepng" name = "lodepng"
version = "3.4.7" version = "3.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24844d5c0b922ddd52fb5bf0964a4c7f8e799a946ec01bb463771eb04fc1a323" checksum = "f0ad39f75bbaa4b10bb6f2316543632a8046a5bcf9c785488d79720b21f044f8"
dependencies = [ dependencies = [
"crc32fast",
"fallible_collections", "fallible_collections",
"flate2", "flate2",
"libc", "libc",
@@ -228,13 +277,21 @@ dependencies = [
] ]
[[package]] [[package]]
name = "miniz_oxide" name = "log"
version = "0.4.4" version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
"cfg-if",
]
[[package]]
name = "miniz_oxide"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
dependencies = [ dependencies = [
"adler", "adler",
"autocfg",
] ]
[[package]] [[package]]
@@ -264,12 +321,24 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "percent-encoding"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]] [[package]]
name = "pico-args" name = "pico-args"
version = "0.4.2" version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468" checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468"
[[package]]
name = "pico-args"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
[[package]] [[package]]
name = "platform" name = "platform"
version = "0.1.0" version = "0.1.0"
@@ -287,10 +356,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
[[package]] [[package]]
name = "rgb" name = "proc-macro2"
version = "0.8.31" version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a374af9a0e5fdcdd98c1c7b64f05004f9ea2555b6c75f211daa81268a3c50f1" checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rgb"
version = "0.8.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3603b7d71ca82644f79b5a06d1220e9a58ede60bd32255f698cb1af8838b8db3"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
] ]
@@ -304,6 +391,37 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "syn"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "thiserror"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "time" name = "time"
version = "0.1.43" version = "0.1.43"
@@ -324,14 +442,67 @@ dependencies = [
] ]
[[package]] [[package]]
name = "upkr" name = "tinyvec"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/exoticorn/upkr.git?rev=2e7983fc#2e7983fc650788d98da2eecef2d16f63e849e4a0" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "unicode-bidi"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58"
[[package]]
name = "unicode-normalization"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-segmentation"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "upkr"
version = "0.2.1"
source = "git+https://github.com/exoticorn/upkr.git?rev=080db40d0088bbee2bdf3c5c75288ac7853d6b7a#080db40d0088bbee2bdf3c5c75288ac7853d6b7a"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cdivsufsort", "cdivsufsort",
"pbr", "lexopt",
"pico-args", "thiserror",
]
[[package]]
name = "url"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
] ]
[[package]] [[package]]
@@ -340,10 +511,11 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"pbr", "pbr",
"pico-args", "pico-args 0.5.0",
"upkr", "upkr",
"wasm-encoder", "walrus",
"wasmparser", "wasm-encoder 0.22.0",
"wasmparser 0.99.0",
] ]
[[package]] [[package]]
@@ -352,6 +524,32 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "walrus"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4eb08e48cde54c05f363d984bb54ce374f49e242def9468d2e1b6c2372d291f8"
dependencies = [
"anyhow",
"id-arena",
"leb128",
"log",
"walrus-macro",
"wasmparser 0.77.0",
]
[[package]]
name = "walrus-macro"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e5bd22c71e77d60140b0bd5be56155a37e5bd14e24f5f87298040d0cc40d7"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.10.2+wasi-snapshot-preview1" version = "0.10.2+wasi-snapshot-preview1"
@@ -360,18 +558,43 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]] [[package]]
name = "wasm-encoder" name = "wasm-encoder"
version = "0.8.0" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db0c351632e46cc06a58a696a6c11e4cf90cad4b9f8f07a0b59128d616c29bb0" checksum = "aa9d9bf45fc46f71c407837c9b30b1e874197f2dc357588430b21e5017d290ab"
dependencies = [
"leb128",
]
[[package]]
name = "wasm-encoder"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef126be0e14bdf355ac1a8b41afc89195289e5c7179f80118e3abddb472f0810"
dependencies = [ dependencies = [
"leb128", "leb128",
] ]
[[package]] [[package]]
name = "wasmparser" name = "wasmparser"
version = "0.81.0" version = "0.77.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98930446519f63d00a836efdc22f67766ceae8dbcc1571379f2bcabc6b2b9abc" checksum = "b35c86d22e720a07d954ebbed772d01180501afe7d03d464f413bb5f8914a8d6"
[[package]]
name = "wasmparser"
version = "0.83.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "718ed7c55c2add6548cca3ddd6383d738cd73b892df400e96b9aa876f0141d7a"
[[package]]
name = "wasmparser"
version = "0.99.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ef3b717afc67f848f412d4f02c127dd3e35a0eecd58c684580414df4fde01d3"
dependencies = [
"indexmap",
"url",
]
[[package]] [[package]]
name = "winapi" name = "winapi"

View File

@@ -6,7 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
curlywas = { git="https://github.com/exoticorn/curlywas.git", rev="196719b" } curlywas = { git="https://github.com/exoticorn/curlywas.git", rev="0e7ea50" }
uw8-tool = { path="../uw8-tool" } uw8-tool = { path="../uw8-tool" }
anyhow = "1" anyhow = "1"
lodepng = "3.4" lodepng = "3.7.2"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

221
platform/src/ges.cwa Normal file
View File

@@ -0,0 +1,221 @@
const GesChannelState.Trigger = 0;
const GesChannelState.EnvState = 1;
const GesChannelState.EnvVol = 2;
const GesChannelState.Phase = 4;
const GesChannelState.Size = 8;
const GesState.Filter = GesChannelState.Size * 4;
const GesState.Size = GesState.Filter + 8*4;
const GesStateOffset = 32;
const GesBufferOffset = 32 + GesState.Size;
export fn sndGes(t: i32) -> f32 {
let baseAddr = 0!0x12c78;
if !(t & 127) {
let i: i32;
loop clearLoop {
(baseAddr + i)!GesBufferOffset = 0;
branch_if (i := i + 4) < 128*4: clearLoop;
}
let ch: i32;
loop channelLoop {
let lazy channelState = baseAddr + GesStateOffset + ch * GesChannelState.Size;
let lazy channelReg = baseAddr + ch * 6;
let envState = channelState?GesChannelState.EnvState;
let envVol = i32.load16_u(channelState, GesChannelState.EnvVol);
let lazy oldTrigger = channelState?GesChannelState.Trigger;
let lazy ctrl = channelReg?0;
if (oldTrigger ^ ctrl) & (ctrl | 2) & 3 {
envState = 1;
envVol = 0;
}
channelState?GesChannelState.Trigger = ctrl;
if envState {
let lazy attack = channelReg?4 & 15;
envVol = envVol + 12 * pow(1.675, (15 - attack) as f32) as i32;
if envVol >= 65535 {
envVol = 65535;
envState = 0;
}
} else {
let inline decay = (channelReg - (ctrl & 1))?5 >> 4;
let inline dec = 8 * pow(1.5625, (15 - decay) as f32) as i32;
envVol = envVol - ((dec * (envVol + 8192)) >> 16);
let inline sustain = (channelReg?5 & 15) << 12;
let lazy targetVol = (ctrl & 1) * sustain;
if envVol < targetVol {
envVol = targetVol;
}
}
channelState?GesChannelState.EnvState = envState;
i32.store16(envVol, channelState, GesChannelState.EnvVol);
let inline note = i32.load16_u(channelReg, 2);
let lazy freq = 440 as f32 * pow(2.0, (note - 69*256) as f32 / (12*256) as f32);
let phaseInc = (freq * (65536.0 / 44100.0)) as i32;
let phase = channelState!GesChannelState.Phase;
let inline pulseWidth = channelReg?1;
let invPhaseInc = 1 as f32 / phaseInc as f32;
i = 0;
let wave = ctrl >> 6;
if wave < 2 {
if wave {
let pulsePhase1 = pulseWidth << 23;
let pulsePhase2 = (511 - pulseWidth) << 23;
loop sawLoop {
let p = (phase ^ 32768) << 16;
let saw = (p >> 16) - polyBlep(phase, invPhaseInc, -32767);
let saw2 = select(p #>= pulsePhase1 & p #< pulsePhase2, -saw, saw);
let saw2 = saw2 -
polyBlep((p - pulsePhase1) >> 16, invPhaseInc, -saw) -
polyBlep((p - pulsePhase2) >> 16, invPhaseInc, saw);
(baseAddr + i)!(GesBufferOffset + 128*4) = saw2;
phase = phase + phaseInc;
branch_if (i := i + 4) < 64*4: sawLoop;
}
}
else
{
let pulsePhase = 32768 + pulseWidth * 128;
loop rectLoop {
(baseAddr + i)!(GesBufferOffset + 128*4) = select((phase & 65535) < pulsePhase, -32768, 32767) -
polyBlep(phase, invPhaseInc, -32767) -
polyBlep(phase - pulsePhase, invPhaseInc, 32767);
phase = phase + phaseInc;
branch_if (i := i + 4) < 64*4: rectLoop;
}
}
} else {
if wave == 2 {
let scale = pulseWidth + 256;
loop triLoop {
let s = phase << 16;
s = (s ^ (s >> 31));
s = (s >> 8) * scale;
s = (s ^ (s >> 31));
(baseAddr + i)!(GesBufferOffset + 128*4) = (s >> 15) - 32768;
phase = phase + phaseInc;
branch_if (i := i + 4) < 64*4: triLoop;
}
} else {
loop noiseLoop {
let s = phase >> 12;
let inline pulse = ((phase >> 8) & 255) >= pulseWidth;
s = s * 0x6746ba73;
s = s ^ (s >> 15) * pulse;
(baseAddr + i)!(GesBufferOffset + 128*4) = (s * 0x835776c7) >> 16;
phase = phase + phaseInc;
branch_if (i := i + 4) < 64*4: noiseLoop;
}
}
}
channelState!GesChannelState.Phase = phase;
if ctrl & 32 {
let lazy modSrc = (ch - 1) & 3;
let inline channelState = baseAddr + GesStateOffset + modSrc * GesChannelState.Size;
let inline channelReg = baseAddr + modSrc * 6;
let inline note = i32.load16_u(channelReg, 2);
let inline freq = 440 as f32 * pow(2.0, (note - 69*256) as f32 / (12*256) as f32);
let phaseInc = (freq * (65536.0 / 44100.0)) as i32;
let phase = channelState!GesChannelState.Phase;
if modSrc < ch {
phase = phase - (phaseInc << 6);
}
i = 0;
loop ringLoop {
let s = phase << 16;
s = (s ^ (s >> 31));
(baseAddr + i)!(GesBufferOffset + 128*4) = ((baseAddr + i)!(GesBufferOffset + 128*4) * ((s >> 15) - 32768)) >> 15;
phase = phase + phaseInc;
branch_if (i := i + 4) < 64*4: ringLoop;
}
}
let channelVol = ((baseAddr + (ch >> 1))?24 >> ((ch & 1) * 4)) & 15;
envVol = envVol * channelVol / 15;
let leftVol = (select(ctrl & 16, 0x3d5b, 0x6a79) >> (ch * 4)) & 15;
let rightVol = 16 - leftVol;
let lazy filter = (ctrl >> 2) & 3;
i = 0;
if filter < 2 {
if filter {
let f = (4096 as f32 - min(4096 as f32, 4096 as f32 * exp(freq * (-8.0 * 3.141 / 44100.0)))) as i32;
let low = (baseAddr + ch * 8)!(GesStateOffset + GesState.Filter);
loop filterLoop {
let in = ((baseAddr + i)!(GesBufferOffset + 128*4) * envVol) >> 18;
low = low + (((in - low) * f) >> 12);
(baseAddr + i * 2)!GesBufferOffset = (baseAddr + i * 2)!GesBufferOffset + ((low * leftVol) >> 4);
(baseAddr + i * 2)!(GesBufferOffset + 4) = (baseAddr + i * 2)!(GesBufferOffset + 4) + ((low * rightVol) >> 4);
branch_if (i := i + 4) < 64*4: filterLoop;
(baseAddr + ch * 8)!(GesStateOffset + GesState.Filter) = low;
(baseAddr + ch * 8)!(GesStateOffset + GesState.Filter + 4) = 0;
}
} else {
loop mixLoop {
let sample = ((baseAddr + i)!(GesBufferOffset + 128*4) * envVol) >> 18;
(baseAddr + i * 2)!GesBufferOffset = (baseAddr + i * 2)!GesBufferOffset + ((sample * leftVol) >> 4);
(baseAddr + i * 2)!(GesBufferOffset + 4) = (baseAddr + i * 2)!(GesBufferOffset + 4) + ((sample * rightVol) >> 4);
branch_if (i := i + 4) < 64*4: mixLoop;
(baseAddr + ch * 8)!(GesStateOffset + GesState.Filter) = sample;
(baseAddr + ch * 8)!(GesStateOffset + GesState.Filter + 4) = 0;
}
}
} else {
filter = filter - 2;
let ctrl = (baseAddr + filter)?26;
let note = i32.load16_u(baseAddr + filter * 2, 28);
let inline freq = 440 as f32 * pow(2.0, (note - 69*256) as f32 / (12*256) as f32);
let F = (8192 as f32 * sin(min(0.25, freq / 44100 as f32) * 3.1415)) as i32;
let Q = 8192 - (ctrl >> 4) * (7000/15);
let Qlimit = (8192*4096/F - F/2) * 3 / 4;
if Q > Qlimit {
Q = Qlimit;
}
let low_out = ctrl & 1;
let high_out = (ctrl >> 1) & 1;
let band_out = (ctrl >> 2) & 1;
let low = (baseAddr + ch * 8)!(GesStateOffset + GesState.Filter);
let band = (baseAddr + ch * 8)!(GesStateOffset + GesState.Filter + 4);
loop filterLoop {
let in = ((baseAddr + i)!(GesBufferOffset + 128*4) * envVol) >> 18;
let high = in - low - ((band * Q) >> 12);
band = band + ((F * high) >> 12);
low = low + ((F * band) >> 12);
let sample = low * low_out + high * high_out + band * band_out;
(baseAddr + i * 2)!GesBufferOffset = (baseAddr + i * 2)!GesBufferOffset + ((sample * leftVol) >> 4);
(baseAddr + i * 2)!(GesBufferOffset + 4) = (baseAddr + i * 2)!(GesBufferOffset + 4) + ((sample * rightVol) >> 4);
branch_if (i := i + 4) < 64*4: filterLoop;
(baseAddr + ch * 8)!(GesStateOffset + GesState.Filter) = low;
(baseAddr + ch * 8)!(GesStateOffset + GesState.Filter + 4) = band;
}
}
branch_if (ch := ch + 1) < 4: channelLoop;
}
}
((baseAddr + (t & 127) * 4)!GesBufferOffset) as f32 / 32768 as f32
}
fn polyBlep(transientPhase: i32, invPhaseInc: f32, magnitude: i32) -> i32 {
let lazy t = ((transientPhase << 16) >> 16) as f32 * invPhaseInc;
let lazy x = max(0 as f32, 1 as f32 - abs(t));
(f32.copysign(x * x, t) * magnitude as f32) as i32
}

View File

@@ -0,0 +1,6 @@
import "env.memory" memory(1);
import "env.sin" fn sin(f32) -> f32;
import "env.pow" fn pow(f32, f32) -> f32;
import "env.exp" fn exp(f32) -> f32;
include "ges.cwa"

View File

@@ -11,13 +11,13 @@ fn main() -> Result<()> {
convert_font()?; convert_font()?;
println!("Compiling loader module"); println!("Compiling loader module");
let loader = curlywas::compile_file("src/loader.cwa", curlywas::Options::default())?; let loader = curlywas::compile_file("src/loader.cwa", curlywas::Options::default()).0?;
File::create("bin/loader.wasm")?.write_all(&loader)?; File::create("bin/loader.wasm")?.write_all(&loader)?;
println!("Loader (including base module): {} bytes", loader.len()); println!("Loader (including base module): {} bytes", loader.len());
println!("Compiling platform module"); println!("Compiling platform module");
let platform = curlywas::compile_file("src/platform.cwa", curlywas::Options::default())?; let platform = curlywas::compile_file("src/platform.cwa", curlywas::Options::default()).0?;
println!("Compressing platform module"); println!("Compressing platform module");
let platform = uw8_tool::pack( let platform = uw8_tool::pack(
&platform, &platform,

View File

@@ -1,6 +1,10 @@
import "env.memory" memory(4); import "env.memory" memory(4);
import "env.sin" fn sin(f32) -> f32;
import "env.cos" fn cos(f32) -> f32; import "env.cos" fn cos(f32) -> f32;
import "env.pow" fn pow(f32, f32) -> f32;
import "env.exp" fn exp(f32) -> f32;
import "env.logChar" fn logChar(i32);
export fn time() -> f32 { export fn time() -> f32 {
(0!64) as f32 / 1000 as f32 (0!64) as f32 / 1000 as f32
@@ -29,11 +33,9 @@ export fn random() -> i32 {
} }
export fn random64() -> i64 { export fn random64() -> i64 {
let state: i64; let lazy state = randomState ^ (randomState #>> 12i64);
randomState = (state := ( let lazy state = state ^ (state << 25i64);
state := randomState ^ (randomState #>> 12i64) randomState = state ^ (state #>> 27i64);
) ^ (state << 25i64)
) ^ (state #>> 27i64);
randomState * 0x2545f4914f6cdd1di64 randomState * 0x2545f4914f6cdd1di64
} }
@@ -59,12 +61,8 @@ export fn cls(col: i32) {
let i: i32; let i: i32;
textCursorX = 0; textCursorX = 0;
textCursorY = 0; textCursorY = 0;
graphicsText = 0; outputChannel = 0;
col = (col & 255) * 0x1010101; memory.fill(120, col, 320*240);
loop pixels {
i!120 = col;
branch_if (i := i + 4) < 320*240: pixels;
}
} }
export fn setPixel(x: i32, y: i32, col: i32) { export fn setPixel(x: i32, y: i32, col: i32) {
@@ -88,12 +86,70 @@ fn clamp(v: i32, min: i32, max: i32) -> i32 {
export fn hline(x1: i32, x2: i32, y: i32, col: i32) { export fn hline(x1: i32, x2: i32, y: i32, col: i32) {
x1 = clamp(x1, 0, 320); x1 = clamp(x1, 0, 320);
x2 = clamp(x2, 0, 320); x2 = clamp(x2, 0, 320);
if x1 < x2 & y #< 240 { if y #>= 240 {
return;
}
let word_start = (x1 + 3) & -4;
let word_end = x2 & -4;
if word_end > word_start {
col = (col & 255) * 0x1010101;
let ptr = y * 320 + x1;
let end = ptr + word_start - x1;
if ptr + 2 <= end {
ptr?120 = col;
ptr?121 = col;
ptr += 2;
}
if ptr < end {
ptr?120 = col;
ptr += 1;
}
end += word_end - word_start;
loop words {
if ptr + 16 <= end {
ptr!120 = col;
ptr!124 = col;
ptr!128 = col;
ptr!132 = col;
ptr += 16;
branch words;
}
if ptr + 8 <= end {
ptr!120 = col;
ptr!124 = col;
ptr += 8;
}
if ptr < end {
ptr!120 = col;
ptr += 4;
}
}
end += x2 - word_end;
if ptr + 2 <= end {
ptr?120 = col;
ptr?121 = col;
ptr += 2;
}
if ptr < end {
ptr?120 = col;
}
} else {
let ptr = y * 320 + x1; let ptr = y * 320 + x1;
let end = ptr + x2 - x1; let end = ptr + x2 - x1;
loop pixels { if ptr + 4 <= end {
ptr?120 = col;
ptr?121 = col;
ptr?122 = col;
ptr?123 = col;
ptr += 4;
}
if ptr + 2 <= end {
ptr?120 = col;
ptr?121 = col;
ptr += 2;
}
if ptr < end {
ptr?120 = col; ptr?120 = col;
branch_if (ptr := ptr + 1) < end: pixels;
} }
} }
} }
@@ -115,7 +171,7 @@ export fn rectangle(x: f32, y: f32, w: f32, h: f32, col: i32) {
} }
} }
export fn rectangle_outline(x: f32, y: f32, w: f32, h: f32, col: i32) { export fn rectangleOutline(x: f32, y: f32, w: f32, h: f32, col: i32) {
let xl = nearest(x) as i32; let xl = nearest(x) as i32;
let xr = nearest(x + w) as i32; let xr = nearest(x + w) as i32;
let yt = nearest(y) as i32; let yt = nearest(y) as i32;
@@ -156,7 +212,7 @@ export fn circle(cx: f32, cy: f32, radius: f32, col: i32) {
} }
} }
export fn circle_outline(cx: f32, cy: f32, radius: f32, col: i32) { export fn circleOutline(cx: f32, cy: f32, radius: f32, col: i32) {
let prev_w: f32; let prev_w: f32;
let y = clamp(nearest(cy - radius) as i32, -1, 241); let y = clamp(nearest(cy - radius) as i32, -1, 241);
let maxY = clamp(nearest(cy + radius) as i32, -1, 241); let maxY = clamp(nearest(cy + radius) as i32, -1, 241);
@@ -256,6 +312,11 @@ export fn line(x1: f32, y1: f32, x2: f32, y2: f32, col: i32) {
p = y1; p = y1;
} }
if max_axis == 0 as f32 {
setPixel(x1 as i32, y1 as i32, col);
return;
}
let steps = floor(p + max_axis) as i32 - floor(p) as i32; let steps = floor(p + max_axis) as i32 - floor(p) as i32;
p = floor(p) + 0.5 - p; p = floor(p) + 0.5 - p;
if max_axis < 0 as f32 { if max_axis < 0 as f32 {
@@ -267,7 +328,7 @@ export fn line(x1: f32, y1: f32, x2: f32, y2: f32, col: i32) {
dy = dy / max_axis; dy = dy / max_axis;
let f = min(max_axis, max(0 as f32, p)); let f = min(max_axis, max(0 as f32, p));
setPixel((x1 + f * dx) as i32, (y1 + f * dy) as i32, col); setPixel(i32.trunc_sat_f32_s(x1 + f * dx), i32.trunc_sat_f32_s(y1 + f * dy), col);
if !steps { if !steps {
return; return;
@@ -280,7 +341,7 @@ export fn line(x1: f32, y1: f32, x2: f32, y2: f32, col: i32) {
loop pixels { loop pixels {
if steps := steps - 1 { if steps := steps - 1 {
setPixel(x1 as i32, y1 as i32, col); setPixel(i32.trunc_sat_f32_s(x1), i32.trunc_sat_f32_s(y1), col);
x1 = x1 + dx; x1 = x1 + dx;
y1 = y1 + dy; y1 = y1 + dy;
branch pixels; branch pixels;
@@ -288,7 +349,93 @@ export fn line(x1: f32, y1: f32, x2: f32, y2: f32, col: i32) {
} }
f = min(max_axis, p) - p; f = min(max_axis, p) - p;
setPixel((x1 + f * dx) as i32, (y1 + f * dy) as i32, col); setPixel(i32.trunc_sat_f32_s(x1 + f * dx), i32.trunc_sat_f32_s(y1 + f * dy), col);
}
export fn blitSprite(sprite: i32, size: i32, x: i32, y: i32, control: i32) {
let lazy width = size & 65535;
let lazy height = select(size >> 16, size >> 16, width);
let lazy x0 = select(x < 0, -x, 0);
let lazy x1 = select(x + width > 320, 320 - x, width);
let lazy y0 = select(y < 0, -y, 0);
let lazy y1 = select(y + height > 240, 240 - y, height);
let lazy numRows = y1 - y0;
let lazy numCols = x1 - x0;
if numRows <= 0 | numCols <= 0 {
return;
}
let trans = (control & 511) - 256;
let lazy flip_x = 1 - ((control >> 8) & 2);
let lazy flip_y = 1 - ((control >> 9) & 2);
if flip_x < 0 {
sprite += width - 1;
}
if flip_y < 0 {
sprite += (height - 1) * width;
}
let spriteRow = sprite + x0 * flip_x + y0 * flip_y * width;
let screenRow = x + x0 + (y + y0) * 320;
loop yloop {
let lx = 0;
loop xloop {
let lazy col = (spriteRow + lx * flip_x)?0;
if col != trans {
(screenRow + lx)?120 = col;
}
branch_if (lx +:= 1) < numCols: xloop;
}
spriteRow += width * flip_y;
screenRow += 320;
branch_if numRows -:= 1: yloop;
}
}
export fn grabSprite(sprite: i32, size: i32, x: i32, y: i32, control: i32) {
let lazy width = size & 65535;
let lazy height = select(size >> 16, size >> 16, width);
let lazy x0 = select(x < 0, -x, 0);
let lazy x1 = select(x + width > 320, 320 - x, width);
let lazy y0 = select(y < 0, -y, 0);
let lazy y1 = select(y + height > 240, 240 - y, height);
let lazy numRows = y1 - y0;
let lazy numCols = x1 - x0;
if numRows <= 0 | numCols <= 0 {
return;
}
let trans = (control & 511) - 256;
let lazy flip_x = 1 - ((control >> 8) & 2);
let lazy flip_y = 1 - ((control >> 9) & 2);
if flip_x < 0 {
sprite += width - 1;
}
if flip_y < 0 {
sprite += (height - 1) * width;
}
let spriteRow = sprite + x0 * flip_x + y0 * flip_y * width;
let screenRow = x + x0 + (y + y0) * 320;
loop yloop {
let lx = 0;
loop xloop {
let lazy col = (screenRow + lx)?120;
if col != trans {
(spriteRow + lx * flip_x)?0 = col;
}
branch_if (lx +:= 1) < numCols: xloop;
}
spriteRow += width * flip_y;
screenRow += 320;
branch_if numRows -:= 1: yloop;
}
} }
////////// //////////
@@ -299,7 +446,8 @@ global mut textCursorX = 0;
global mut textCursorY = 0; global mut textCursorY = 0;
global mut textColor = 15; global mut textColor = 15;
global mut bgColor = 0; global mut bgColor = 0;
global mut graphicsText = 0; global mut outputChannel = 0;
global mut textScale = 1;
export fn printChar(char: i32) { export fn printChar(char: i32) {
loop chars { loop chars {
@@ -311,6 +459,13 @@ export fn printChar(char: i32) {
global mut controlCodeLength = 0; global mut controlCodeLength = 0;
fn printSingleChar(char: i32) { fn printSingleChar(char: i32) {
let charSize = 8 * textScale;
if outputChannel >= 2 & (char < 4 | char > 6) {
logChar(char);
return;
}
controlCodeLength?0x12d20 = char; controlCodeLength?0x12d20 = char;
controlCodeLength = controlCodeLength + 1; controlCodeLength = controlCodeLength + 1;
char = 0x12d20?0; char = 0x12d20?0;
@@ -324,54 +479,61 @@ fn printSingleChar(char: i32) {
return; return;
} }
if char == 4 | char == 5 { if char >= 4 & char <= 6 {
graphicsText = char == 5; outputChannel = char - 4;
if !outputChannel {
textCursorX = 0; textCursorX = 0;
textCursorY = 0; textCursorY = 0;
}
return;
}
if char == 7 {
80?0 = 80?0 ^ 2;
return; return;
} }
if char == 8 { if char == 8 {
textCursorX = textCursorX - 8; textCursorX = textCursorX - charSize;
if !graphicsText & textCursorX < 0 { if !outputChannel & textCursorX < 0 {
textCursorX = 320-8; textCursorX = 320-charSize;
printSingleChar(11); printSingleChar(11);
} }
return; return;
} }
if char == 9 { if char == 9 {
if !graphicsText & textCursorX >= 320 { if !outputChannel & textCursorX >= 320 {
printChar(0xd0a); printChar(0xd0a);
} }
textCursorX = textCursorX + 8; textCursorX = textCursorX + charSize;
return; return;
} }
if char == 10 { if char == 10 {
textCursorY = textCursorY + 8; textCursorY = textCursorY + charSize;
if !graphicsText & textCursorY >= 240 { if !outputChannel & textCursorY >= 240 {
textCursorY = 240 - 8; textCursorY = 240 - charSize;
let i: i32; let i: i32;
loop scroll_copy { loop scroll_copy {
i!120 = i!(120 + 320 * 8); i!120 = (i + 320 * charSize)!120;
branch_if (i := i + 4) < 320 * (240 - 8): scroll_copy; branch_if (i := i + 4) < 320 * (240 - charSize): scroll_copy;
} }
rectangle(0 as f32, (240 - 8) as f32, 320 as f32, 8 as f32, bgColor); rectangle(0 as f32, (240 - charSize) as f32, 320 as f32, charSize as f32, bgColor);
} }
return; return;
} }
if char == 11 { if char == 11 {
textCursorY = textCursorY - 8; textCursorY = textCursorY - charSize;
if !graphicsText & textCursorY < 0 { if !outputChannel & textCursorY < 0 {
textCursorY = 0; textCursorY = 0;
let i = 320 * (240 - 8); let i = 320 * (240 - charSize);
loop scroll_copy { loop scroll_copy {
i!(116 + 320 * 8) = i!116; (i + 320 * charSize)!116 = i!116;
branch_if (i := i - 4): scroll_copy; branch_if (i := i - 4): scroll_copy;
} }
rectangle(0 as f32, 0 as f32, 320 as f32, 8 as f32, bgColor); rectangle(0 as f32, 0 as f32, 320 as f32, charSize as f32, bgColor);
} }
return; return;
} }
@@ -403,9 +565,15 @@ fn printSingleChar(char: i32) {
return; return;
} }
if char == 30 {
let scale = 0x12d20?1;
textScale = select(scale > 0 & scale <= 16, scale, 1);
return;
}
if char == 31 { if char == 31 {
textCursorX = 0x12d20?1 * (8 - graphicsText * 6); textCursorX = 0x12d20?1 * (8 - outputChannel * 6);
textCursorY = 0x12d20?2 * (8 - graphicsText * 7); textCursorY = 0x12d20?2 * (8 - outputChannel * 7);
return; return;
} }
@@ -425,35 +593,37 @@ data(0x12d00) {
1, 1, 1, 1, // 16-19, 1, 1, 1, 1, // 16-19,
1, 1, 1, 1, // 20-23, 1, 1, 1, 1, // 20-23,
1, 1, 1, 1, // 24-27, 1, 1, 1, 1, // 24-27,
1, 1, 1, 3 // 28-31 1, 1, 2, 3 // 28-31
) )
} }
fn drawChar(char: i32) { fn drawChar(char: i32) {
if !graphicsText & textCursorX >= 320 { if !outputChannel & textCursorX >= 320 {
printChar(0xd0a); printChar(0xd0a);
} }
let charSize = 8 * textScale;
let y: i32; let y: i32;
loop rows { loop rows {
let bits = (char * 8 + y)?0x13400; let bits = (char * 8 + y / textScale)?0x13400;
let x = 0; let x = 0;
if graphicsText { if outputChannel {
loop pixels { loop pixels {
if (bits := bits << 1) & 256 { if (bits << (x / textScale)) & 128 {
setPixel(textCursorX + x, textCursorY + y, textColor); setPixel(textCursorX + x, textCursorY + y, textColor);
} }
branch_if (x := x + 1) < 8: pixels; branch_if (x := x + 1) < charSize: pixels;
} }
} else { } else {
loop pixels { loop pixels {
setPixel(textCursorX + x, textCursorY + y, select((bits := bits << 1) & 256, textColor, bgColor)); setPixel(textCursorX + x, textCursorY + y, select((bits << (x / textScale)) & 128, textColor, bgColor));
branch_if (x := x + 1) < 8: pixels; branch_if (x := x + 1) < charSize: pixels;
} }
} }
branch_if (y := y + 1) < 8: rows; branch_if (y := y + 1) < charSize: rows;
} }
textCursorX = textCursorX + 8; textCursorX = textCursorX + charSize;
} }
export fn printString(ptr: i32) { export fn printString(ptr: i32) {
@@ -490,11 +660,37 @@ export fn setBackgroundColor(col: i32) {
} }
export fn setCursorPosition(x: i32, y: i32) { export fn setCursorPosition(x: i32, y: i32) {
let lazy scale = select(graphicsText, 1, 8); let lazy scale = select(outputChannel, 1, 8);
textCursorX = x * scale; textCursorX = x * scale;
textCursorY = y * scale; textCursorY = y * scale;
} }
///////////
// SOUND //
///////////
include "ges.cwa"
export fn playNote(channel: i32, note: i32) {
(channel * 6)?80 = (channel * 6)?80 & 0xfe ^ if note {
(channel * 6)?83 = note & 127;
2 | !(note >> 7)
} else {
0
};
}
data 80 {
i8(
0x80, 0xc0, 0, 81, 0xa0, 0x50,
0xc4, 0, 0, 69, 0x60, 0x40,
0x44, 0xb0, 0, 69, 0x90, 0x43,
0x4, 0xf0, 0, 69, 0xa4, 0x44,
0xff, 0xff,
1, 1, 0, 100, 0, 100
)
}
/////////// ///////////
// SETUP // // SETUP //
/////////// ///////////
@@ -535,10 +731,19 @@ start fn setup() {
branch_if (i := i - 1) >= 0: expand_sweetie; branch_if (i := i - 1) >= 0: expand_sweetie;
} }
memory.fill(0, 0, 64);
memory.fill(112, 0, 8);
memory.fill(0x14000, 0, 0x2c000);
cls(0); cls(0);
randomSeed(random()); randomSeed(random());
} }
data 0x12c78 {
i32(80)
}
data 0x13000+192*4 { data 0x13000+192*4 {
i32( i32(
0x2c1c1a, 0x2c1c1a,

View File

@@ -36,6 +36,8 @@ for dir in build/*; do
cp $example $dir/examples cp $example $dir/examples
done done
cp -r ../examples/include $dir/include
mkdir $dir/carts mkdir $dir/carts
for example in $dir/examples/*; do for example in $dir/examples/*; do
build/microw8-linux/uw8 pack -l 9 $example $dir/carts/$(basename ${example%.*}).uw8 build/microw8-linux/uw8 pack -l 9 $example $dir/carts/$(basename ${example%.*}).uw8

View File

@@ -14,12 +14,14 @@ The initial motivation behind MicroW8 was to explore whether there was a way to
* Memory: 256KB * Memory: 256KB
* Gamepad input (D-Pad + 4 Buttons) * Gamepad input (D-Pad + 4 Buttons)
For detailed [documentation see here](docs).
## Examples ## Examples
* [Fireworks](v0.1.1#AgwvgP+M59snqjl4CMKw5sqm1Zw9yJCbSviMjeLUdHus2a3yl/a99+uiBeqZgP/2jqSjrLjRk73COMM6OSLpsxK8ugT1kuk/q4hQUqqPpGozHoa0laulzGGcahzdfdJsYaK1sIdeIYS9M5PnJx/Wk9H+PvWEPy2Zvv7I6IW7Fg==) (127 bytes): Some fireworks to welcome 2022. * [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
* [Skip Ahead](v0.1.1#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 * [Fireworks](v0.2.0#AgwvgP+M59snqjl4CMKw5sqm1Zw9yJCbSviMjeLUdHus2a3yl/a99+uiBeqZgP/2jqSjrLjRk73COMM6OSLpsxK8ugT1kuk/q4hQUqqPpGozHoa0laulzGGcahzdfdJsYaK1sIdeIYS9M5PnJx/Wk9H+PvWEPy2Zvv7I6IW7Fg==) (127 bytes): Some fireworks to welcome 2022.
* [OhNoAnotherTunnel](v0.1.1#AgPP1oEFvPzY/rBZwTumtYn37zeMFgpir1Bkn91jsNcp26VzoUpkAOOJTtnzVBfW+/dGnnIdbq/irBUJztY5wuua80DORTYZndgdwZHcSk15ajc4nyO0g1A6kGWyW56oZk0iPYJA9WtUmoj0Plvy1CGwIZrMe57X7QZcdqc3u6zjTA41Tpiqi9vnO3xbhi8o594Vx0XPXwVzpYq1ZCTYenfAGaXKkDmAFJqiVIsiCg==) (175 bytes): A port of my [entry](http://tic80.com/play?cart=1871) in the Outline'21 bytebattle final * [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
* [Technotunnel](v0.1.1#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 * [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
* [Font & Palette](v0.1.1#At/p39+IBnj6ry1TRe7jzVy2A4tXgBvmoW2itzoyF2aM28pGy5QDiKxqrk8l9sbWZLtnAb+jgOfU+9QhpuyCAkhN6gPOU481IUL/df96vNe3h288Dqwhd3sfFpothIVFsMwRK72kW2hiR7zWsaXyy5pNmjR6BJk4piWx9ApT1ZwoUajhk6/zij6itq/FD1U3jj/J3MOwqZ2ef8Bv6ZPQlJIYVf62icGa69wS6SI1qBpIFiF14F8PcztRVbKIxLpT4ArCS6nz6FPnyUkqATGSBNPJ): Just a simple viewer for the default font and palette. * [Font & Palette](v0.2.0#At/p39+IBnj6ry1TRe7jzVy2A4tXgBvmoW2itzoyF2aM28pGy5QDiKxqrk8l9sbWZLtnAb+jgOfU+9QhpuyCAkhN6gPOU481IUL/df96vNe3h288Dqwhd3sfFpothIVFsMwRK72kW2hiR7zWsaXyy5pNmjR6BJk4piWx9ApT1ZwoUajhk6/zij6itq/FD1U3jj/J3MOwqZ2ef8Bv6ZPQlJIYVf62icGa69wS6SI1qBpIFiF14F8PcztRVbKIxLpT4ArCS6nz6FPnyUkqATGSBNPJ): Just a simple viewer for the default font and palette.
Examplers for older versions: Examplers for older versions:
@@ -29,27 +31,51 @@ Examplers for older versions:
## Versions ## Versions
### v0.1.1 ### v0.2.2
* [Web runtime](v0.1.1) * [Web runtime](v0.2.2)
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.1.1/microw8-0.1.1-linux.tgz) * [Linux](https://github.com/exoticorn/microw8/releases/download/v0.2.2/microw8-0.2.2-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.1.1/microw8-0.1.1-macos.tgz) * [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.2.2/microw8-0.2.2-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.1.1/microw8-0.1.1-windows.zip) * [Windows](https://github.com/exoticorn/microw8/releases/download/v0.2.2/microw8-0.2.2-windows.zip)
Changes: Changes:
* implement more robust file watcher * call `start` function after loading cart if the cart exports one
* add basic video recording on F10 in web runtime * fix `sndGes` having the wrong name and not being included in the auto imports
* add screenshot on F9 * fix control codes 4-6 (change text output mode) being invoked when used as parameters in other control sequences
* add watchdog to interrupt hanging update in native runtime * only open browser window once a cart was compiled sucessfully when running with `-b`
* add devkit mode to web runtime
* add unpack and compile commands to uw8
* add support for table/element section in pack command
* disable wayland support (caused missing window decorations in gnome)
### v0.1.0 ### v0.2.1
* [Web runtime](v0.1.0) * [Web runtime](v0.2.1)
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.1.0/microw8-0.1.0-linux.tgz) * [Linux](https://github.com/exoticorn/microw8/releases/download/v0.2.1/microw8-0.2.1-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.1.0/microw8-0.1.0-macos.tgz) * [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.2.1/microw8-0.2.1-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.1.0/microw8-0.1.0-windows.zip) * [Windows](https://github.com/exoticorn/microw8/releases/download/v0.2.1/microw8-0.2.1-windows.zip)
Changes:
* new gpu accelerated renderer with (optional) crt filter
* optimized `hline` function, a big speed-up when drawing large filled circles or rectangles
* print fractional size of packed `uw8` cart
### v0.2.0
* [Web runtime](v0.2.0)
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.2.0/microw8-0.2.0-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.2.0/microw8-0.2.0-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.2.0/microw8-0.2.0-windows.zip)
Changes:
* [add sound support!](docs#sound)
* add support to redirect text output to the console for debugging using control code 6
* update curlywas:
* add support for `else if`
* add support for escape sequences in strings
* add support for char literals
* add support for binop-assignment, eg. `+=`, `^=`, `<<=` etc. (also support for the tee operator: `+:=`)
* "integer constant cast to float" literal syntax in CurlyWas (ex. `1_f` is equivalent to `1 as f32`)
### Older versions
[Find older versions here.](versions)

View File

@@ -5,22 +5,28 @@ description = "Docs"
# Overview # Overview
MicroW8 loads WebAssembly modules with a maximum size of 256kb. You module needs to export MicroW8 loads WebAssembly modules with a maximum size of 256kb. Your module needs to export
a function `fn upd()` which will be called once per frame. a function `fn upd()` which will be called once per frame.
After calling `upd` MicroW8 will display the 320x240 8bpp framebuffer located After calling `upd` MicroW8 will display the 320x240 8bpp framebuffer located
at offset 120 in memory with the 32bpp palette located at 0x13000. at offset 120 in memory with the 32bpp palette located at 0x13000.
The memory has to be imported as `env` `memory` and has a maximum size of 256kb (4 pages). The memory has to be imported as `env` `memory` and has a maximum size of 256kb (4 pages).
If the module exports a function called `start`, it will be called once after the module is
loaded.
# Memory map # Memory map
``` ```
00000-00040: user memory 00000-00040: user memory
00040-00044: time since module start in ms 00040-00044: time since module start in ms
00044-0004c: gamepad state 00044-0004c: gamepad state
0004c-00078: reserved 0004c-00050: reserved
00050-00070: sound data (synced to sound thread)
00070-00078: reserved
00078-12c78: frame buffer 00078-12c78: frame buffer
12c78-13000: reserved 12c78-12c7c: sound registers/work area base address (for sndGes function)
12c7c-13000: reserved
13000-13400: palette 13000-13400: palette
13400-13c00: font 13400-13c00: font
13c00-14000: reserved 13c00-14000: reserved
@@ -47,7 +53,7 @@ Returns the arccosine of `x`.
Returns the arctangent of `x`. Returns the arctangent of `x`.
### fn atan2(y: f32, y: f32) -> f32 ### fn atan2(y: f32, x: f32) -> f32
Returns the angle between the point `(x, y)` and the positive x-axis. Returns the angle between the point `(x, y)` and the positive x-axis.
@@ -140,13 +146,13 @@ Fills the circle at `cx, cy` and with `radius` with the given color index.
(Sets all pixels where the pixel center lies inside the circle.) (Sets all pixels where the pixel center lies inside the circle.)
### fn rectangle_outline(x: f32, y: f32, w: f32, h: f32, color: i32) ### fn rectangleOutline(x: f32, y: f32, w: f32, h: f32, color: i32)
Draws a one pixel outline on the inside of the given rectangle. Draws a one pixel outline on the inside of the given rectangle.
(Draws the outermost pixels that are still inside the rectangle area.) (Draws the outermost pixels that are still inside the rectangle area.)
### fn circle_outline(cx: f32, cy: f32, radius: f32, color: i32) ### fn circleOutline(cx: f32, cy: f32, radius: f32, color: i32)
Draws a one pixel outline on the inside of the given circle. Draws a one pixel outline on the inside of the given circle.
@@ -156,6 +162,21 @@ Draws a one pixel outline on the inside of the given circle.
Draws a line from `x1,y1` to `x2,y2` in the given color index. Draws a line from `x1,y1` to `x2,y2` in the given color index.
### fn blitSprite(spriteData: i32, size: i32, x: i32, y: i32, control: i32)
Copies the pixel data at `spriteData` onto the screen at `x`, `y`. The size of the sprite is passed as `width | (height << 16)`.
If the height is given as 0, the sprite is is treated as square (width x width).
The control parameter controls masking and flipping of the sprite:
* bits 0-7: color mask index
* bit 8: switch on masked blit (pixel with color mask index are treated as transparent)
* bit 9: flip sprite x
* bit 10: flip sprite y
### fn grabSprite(spriteData: i32, size: i32, x: i32, y: i32, control: i32)
Copies the pixel data on the screen at `x`, `y` to `spriteData`. Parameters are exactly the same as `blitSprite`.
## Input ## Input
MicroW8 provides input from a gamepad with one D-Pad and 4 buttons, or a keyboard emulation thereof. MicroW8 provides input from a gamepad with one D-Pad and 4 buttons, or a keyboard emulation thereof.
@@ -223,13 +244,14 @@ Characters 0-31 are control characters and don't print by default. They take the
Avoid the reserved control chars, they are currently NOPs but their behavior can change in later MicroW8 versions. Avoid the reserved control chars, they are currently NOPs but their behavior can change in later MicroW8 versions.
| Code | Parameters | Operation | | Code | Parameters | Operation |
| ----- | ---------- | ------------------------------------ | | ----- | ---------- | ------------------------------------------ |
| 0 | - | Nop | | 0 | - | Nop |
| 1 | char | Print char (including control chars) | | 1 | char | Print char (including control chars) |
| 2-3 | - | Reserved | | 2-3 | - | Reserved |
| 4 | - | Switch to normal mode | | 4 | - | Switch to normal mode, reset cursor to 0,0 |
| 5 | - | Switch to graphics mode | | 5 | - | Switch to graphics mode |
| 6-7 | - | Reserved | | 6 | - | Switch output to (debug) console |
| 7 | - | Bell / trigger sound channel 0 |
| 8 | - | Move cursor left | | 8 | - | Move cursor left |
| 9 | - | Move cursor right | | 9 | - | Move cursor right |
| 10 | - | Move cursor down | | 10 | - | Move cursor down |
@@ -245,6 +267,21 @@ Avoid the reserved control chars, they are currently NOPs but their behavior can
(*) In graphics mode, the x coordinate is doubled when using control char 31 to be able to cover the whole screen with one byte. (*) In graphics mode, the x coordinate is doubled when using control char 31 to be able to cover the whole screen with one byte.
#### Debug output
Control code 6 switches all text output (except codes 4 and 5 to switch output back to the screen) to the console. Where exactly this ends
up (if at all) is an implementation detail of the runtimes. The native dev-runtime writes the debug output to `stdout`, the web runtime to
the debug console using `console.log`. Both implementation buffer the output until they encounter a newline character (10) in the output stream.
There may be future runtimes that ignore the debug output completely.
In CurlyWas, a simple way to log some value might look like this:
```
printChar('\06V: '); // switch to console out, print some prefix
printInt(some_value);
printChar('\n\4'); // newline and switch back to screen
```
### fn printChar(char: i32) ### fn printChar(char: i32)
Prints the character in the lower 8 bits of `char`. If the upper 24 bits are non-zero, right-shifts `char` by 8 bits and loops back to the beginning. Prints the character in the lower 8 bits of `char`. If the upper 24 bits are non-zero, right-shifts `char` by 8 bits and loops back to the beginning.
@@ -269,6 +306,130 @@ Sets the background color.
Sets the cursor position. In normal mode `x` and `y` are multiplied by 8 to get the pixel position, in graphics mode they are used as is. Sets the cursor position. In normal mode `x` and `y` are multiplied by 8 to get the pixel position, in graphics mode they are used as is.
## Sound
### Low level operation
MicroW8 actually runs two instances of your module. On the first instance, it calls `upd` and displays the framebuffer found in its memory. On the
second instance, it calls `snd` instead with an incrementing sample index and expects that function to return sound samples for the left and right
channel at 44100 Hz. If your module does not export a `snd` function, it calls the api function `sndGes` instead.
As the only means of communication, 32 bytes starting at address 0x00050 are copied from main to sound memory after `upd` returns.
By default, the `sndGes` function generates sound based on the 32 bytes at 0x00050. This means that in the default configuration those 32 bytes act
as sound registers. See the `sndGes` function for the meaning of those registers.
### export fn snd(sampleIndex: i32) -> f32
If the module exports a `snd` function, it is called 88200 times per second to provide PCM sample data for playback (44.1kHz stereo).
The `sampleIndex` will start at 0 and increments by 1 for each call. On even indices the function is expected to return a sample value for
the left channel, on odd indices for the right channel.
### fn playNote(channel: i32, note: i32)
Triggers a note (1-127) on the given channel (0-3). Notes are semitones with 69 being A4 (same as MIDI). A note value of 0 stops the
sound playing on that channel. A note value 128-255 will trigger note-128 and immediately stop it (playing attack+release parts of envelope).
This function assumes the default setup, with the `sndGes` registers located at 0x00050.
### fn sndGes(sampleIndex: i32) -> f32
This implements a sound chip, generating sound based on 32 bytes of sound registers.
The spec of this sound chip are:
- 4 channels with individual volume control (0-15)
- rect, saw, tri, noise wave forms selectable per channel
- each wave form supports some kind of pulse width modulation
- each channel has an optional automatic low pass filter, or can be sent to one of two manually controllable filters
- each channel can select between a narrow and a wide stereo positioning. The two stereo positions of each channel are fixed.
- optional ring modulation
This function requires 1024 bytes of working memory, the first 32 bytes of which are interpreted as the sound registers.
The base address of its working memory can be configured by writing the address to 0x12c78. It defaults to 0x00050.
Here is a short description of the 32 sound registers.
```
00 - CTRL0
06 - CTRL1
0c - CTRL2
12 - CTRL3
| 7 6 | 5 | 4 | 3 2 | 1 | 0 |
| wave | ring | wide | filter | trigger | note on |
note on: stay in decay/sustain part of envelope
trigger: the attack part of the envlope is triggered when either this changes
or note on is changed from 0 to 1.
filter : 0 - no filter
1 - fixed 6db 1-pole filter with cutoff two octaves above note
2 - programmable filter 0
3 - programmable filter 1
wide : use wide stereo panning
ring : ring modulate with triangle wave at frequency of previous channel
wave : 0 - rectangle
1 - saw
2 - triangle
3 - noise
01 - PULS0
07 - PULS1
0d - PULS2
13 - PULS3
Pulse width 0-255, with 0 being the plain version of each wave form.
rectangle - 50%-100% pulse width
saw - inverts 0%-100% of the saw wave form around the center
triangle - morphs into an octave up triangle wave
noise - blends into a decimated saw wave (just try it out)
02 - FINE0
08 - FINE1
0e - FINE2
14 - FINE3
Fractional note
03 - NOTE0
09 - NOTE1
0f - NOTE2
15 - NOTE3
Note, 69 = A4
04 - ENVA0
0a - ENVA1
10 - ENVA2
16 - ENVA3
| 7 6 5 4 | 3 2 1 0 |
| decay | attack |
05 - ENVB0
0b - ENVB1
11 - ENVB2
17 - ENVB3
| 7 6 5 4 | 3 2 1 0 |
| release | sustain |
18 - VO01
| 7 6 5 4 | 3 2 1 0 |
| volume 1 | volume 0 |
19 - VO23
| 7 6 5 4 | 3 2 1 0 |
| volume 3 | volume 2 |
1a - FCTR0
1b - FCTR1
| 7 6 5 4 | 3 | 2 | 1 | 0 |
| resonance | 0 | band | high | low |
1c - FFIN0
1e - FFIN1
cutoff frequency - fractional note
1d - FNOT0
1f - FNOT1
cutoff frequency - note
```
# The `uw8` tool # The `uw8` tool
The `uw8` tool included in the MicroW8 download includes a number of useful tools for developing MicroW8 carts. For small productions written in The `uw8` tool included in the MicroW8 download includes a number of useful tools for developing MicroW8 carts. For small productions written in
@@ -284,6 +445,7 @@ Runs `<file>` which can be a binary WebAssembly module, an `.uw8` cart, a wat (W
Options: Options:
* `-b`, `--browser`: Run in browser instead of using native runtime
* `-t FRAMES`, `--timeout FRAMES`: Sets the timeout in frames (1/60s). If the start or update function runs longer than this it is forcibly interupted * `-t FRAMES`, `--timeout FRAMES`: Sets the timeout in frames (1/60s). If the start or update function runs longer than this it is forcibly interupted
and execution of the cart is stopped. Defaults to 30 (0.5s) and execution of the cart is stopped. Defaults to 30 (0.5s)
* `-w`, `--watch`: Reloads the given file every time it changes on disk. * `-w`, `--watch`: Reloads the given file every time it changes on disk.
@@ -292,6 +454,30 @@ and execution of the cart is stopped. Defaults to 30 (0.5s)
* `-l LEVEL`, `--level LEVEL`: Compression level (0-9). Higher compression levels are really slow. * `-l LEVEL`, `--level LEVEL`: Compression level (0-9). Higher compression levels are really slow.
* `-o FILE`, `--output FILE`: Write the loaded and optionally packed cart back to disk. * `-o FILE`, `--output FILE`: Write the loaded and optionally packed cart back to disk.
when using the native runtime:
* `-m`, `--no-audio`: Disable audio, also reduces cpu load a bit
* `--no-gpu`: Force old cpu-only window code
* `--filter FILTER`: Select an upscale filter at startup
* `--fullscreen`: Start in fullscreen mode
Note that the cpu-only window does not support fullscreen nor upscale filters.
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.
Therefore you should rarely need to manually pass --no-gpu. If you prefer the old pixel doubling look to the now default crt filter,
you can just pass `--filter nearest` or `--filter 1`.
The upscale filter options are:
```
1, nearest : Anti-aliased nearest filter
2, fast_crt : Very simple, cheap crt filter, not very good below a window size of 960x720
3, ss_crt : Super sampled crt filter, a little more demanding on the GPU but scales well to smaller window sizes
4, chromatic_crt : Variant of fast_crt with a slight offset of the three color dots of a pixel, still pretty cheap
5, auto_crt (default) : ss_crt below 960x720, chromatic_crt otherwise
```
You can switch the upscale filter at any time using the keys 1-5. You can toggle fullscreen with F.
## `uw8 pack` ## `uw8 pack`
Usage: Usage:

105
site/content/versions.md Normal file
View File

@@ -0,0 +1,105 @@
+++
description = "Versions"
+++
### v0.2.0
* [Web runtime](v0.2.0)
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.2.0/microw8-0.2.0-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.2.0/microw8-0.2.0-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.2.0/microw8-0.2.0-windows.zip)
Changes:
* [add sound support!](docs#sound)
* add support to redirect text output to the console for debugging using control code 6
* update curlywas:
* add support for `else if`
* add support for escape sequences in strings
* add support for char literals
* add support for binop-assignment, eg. `+=`, `^=`, `<<=` etc. (also support for the tee operator: `+:=`)
* "integer constant cast to float" literal syntax in CurlyWas (ex. `1_f` is equivalent to `1 as f32`)
### v0.2.0-rc3
* [Web runtime](v0.2.0-rc3)
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc3/microw8-0.2.0-rc3-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc3/microw8-0.2.0-rc3-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc3/microw8-0.2.0-rc3-windows.zip)
Changes:
* improve timing stability some more. essentially now guaranteeing that "frame = time_ms * 6 / 100" returns
consecutive frame numbers, provided the module can be run at 60 fps
* add support to redirect text output to the console for debugging using control code 6
* update curlywas:
* add support for `else if`
* add support for escape sequences in strings
* add support for char literals
* add support for binop-assignment, eg. `+=`, `^=`, `<<=` etc. (also support for the tee operator: `+:=`)
### v0.2.0-rc2
* [Web runtime](v0.2.0-rc2)
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc2/microw8-0.2.0-rc2-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc2/microw8-0.2.0-rc2-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc2/microw8-0.2.0-rc2-windows.zip)
Changes:
* fix timing issues of sound playback, especially on systems with large sound buffers
### v0.2.0-rc1
* [Web runtime](v0.2.0-rc1)
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc1/microw8-0.2.0-rc1-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc1/microw8-0.2.0-rc1-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.2.0-rc1/microw8-0.2.0-rc1-windows.zip)
Changes:
* [add sound support](docs#sound)
* "integer constant cast to float" literal syntax in CurlyWas (ex. `1_f` is equivalent to `1 as f32`)
Known issues:
* timing accuracy/update frequency of sound support currently depends on sound buffer size
### v0.1.2
* [Web runtime](v0.1.2)
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.1.2/microw8-0.1.2-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.1.2/microw8-0.1.2-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.1.2/microw8-0.1.2-windows.zip)
Changes:
* add option to `uw8 run` to run the cart in the browser using the web runtime
* CurlyWas: implement `include` support
* CurlyWas: implement support for constants
* fix crash when trying to draw zero sized line
### v0.1.1
* [Web runtime](v0.1.1)
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.1.1/microw8-0.1.1-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.1.1/microw8-0.1.1-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.1.1/microw8-0.1.1-windows.zip)
Changes:
* implement more robust file watcher
* add basic video recording on F10 in web runtime
* add screenshot on F9
* add watchdog to interrupt hanging update in native runtime
* add devkit mode to web runtime
* add unpack and compile commands to uw8
* add support for table/element section in pack command
* disable wayland support (caused missing window decorations in gnome)
### v0.1.0
* [Web runtime](v0.1.0)
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.1.0/microw8-0.1.0-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.1.0/microw8-0.1.0-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.1.0/microw8-0.1.0-windows.zip)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -4,7 +4,7 @@
<section> <section>
<h1 class="text-center heading-text">A WebAssembly based fantasy console</h1> <h1 class="text-center heading-text">A WebAssembly based fantasy console</h1>
</section> </section>
<a href="v0.1.1"> <a href="v0.2.2">
<img class="demonstration-gif" style="width:640px;height:480px;image-rendering:pixelated" src="img/technotunnel.png"></img> <img class="demonstration-gif" style="width:640px;height:480px;image-rendering:pixelated" src="img/technotunnel.png"></img>
</a> </a>
</div> </div>

View File

@@ -1,36 +1,67 @@
use anyhow::{bail, Result}; use anyhow::{anyhow, bail, Result};
use notify::{DebouncedEvent, Watcher, RecommendedWatcher}; use notify_debouncer_mini::{
use std::{ new_debouncer,
collections::BTreeSet, notify::{self, RecommendedWatcher},
path::{Path, PathBuf}, DebouncedEvent, DebouncedEventKind, Debouncer,
sync::mpsc,
time::Duration,
}; };
use std::{collections::BTreeSet, path::PathBuf, sync::mpsc, time::Duration};
pub struct FileWatcher { pub struct FileWatcher {
_watcher: RecommendedWatcher, debouncer: Debouncer<RecommendedWatcher>,
watched_files: BTreeSet<PathBuf>, watched_files: BTreeSet<PathBuf>,
directories: BTreeSet<PathBuf>,
rx: mpsc::Receiver<DebouncedEvent>, rx: mpsc::Receiver<DebouncedEvent>,
} }
pub struct FileWatcherBuilder(BTreeSet<PathBuf>);
impl FileWatcher { impl FileWatcher {
pub fn builder() -> FileWatcherBuilder { pub fn new() -> Result<FileWatcher> {
FileWatcherBuilder(BTreeSet::new()) let (tx, rx) = mpsc::channel();
let debouncer = new_debouncer(Duration::from_millis(100), move |res| match res {
Ok(events) => {
for event in events {
let _ = tx.send(event);
}
}
Err(err) => {
eprintln!("Error watching for file changes: {err}");
}
})?;
Ok(FileWatcher {
debouncer,
watched_files: BTreeSet::new(),
directories: BTreeSet::new(),
rx,
})
}
pub fn add_file<P: Into<PathBuf>>(&mut self, path: P) -> Result<()> {
let path = path.into();
let parent = path.parent().ok_or_else(|| anyhow!("File has no parent"))?;
if !self.directories.contains(parent) {
self.debouncer
.watcher()
.watch(parent, notify::RecursiveMode::NonRecursive)?;
self.directories.insert(parent.to_path_buf());
}
self.watched_files.insert(path);
Ok(())
} }
pub fn poll_changed_file(&self) -> Result<Option<PathBuf>> { pub fn poll_changed_file(&self) -> Result<Option<PathBuf>> {
let event = self.rx.try_recv(); match self.rx.try_recv() {
match event { Ok(event) => match event.kind {
Ok(DebouncedEvent::Create(path) | DebouncedEvent::Write(path)) => { DebouncedEventKind::Any => {
let handle = same_file::Handle::from_path(&path)?; let handle = same_file::Handle::from_path(&event.path)?;
for file in &self.watched_files { for file in &self.watched_files {
if handle == same_file::Handle::from_path(file)? { if handle == same_file::Handle::from_path(file)? {
return Ok(Some(path)); return Ok(Some(event.path));
} }
} }
} }
_ => (),
},
Err(mpsc::TryRecvError::Disconnected) => bail!("File watcher disconnected"), Err(mpsc::TryRecvError::Disconnected) => bail!("File watcher disconnected"),
_ => (), _ => (),
} }
@@ -38,33 +69,3 @@ impl FileWatcher {
Ok(None) Ok(None)
} }
} }
impl FileWatcherBuilder {
pub fn add_file<P: Into<PathBuf>>(&mut self, path: P) -> &mut Self {
self.0.insert(path.into());
self
}
pub fn build(self) -> Result<FileWatcher> {
let mut directories: BTreeSet<&Path> = BTreeSet::new();
for file in &self.0 {
if let Some(directory) = file.parent() {
directories.insert(directory);
}
}
let (tx, rx) = mpsc::channel();
let mut watcher = notify::watcher(tx, Duration::from_millis(100))?;
for directory in directories {
watcher.watch(directory, notify::RecursiveMode::NonRecursive)?;
}
Ok(FileWatcher {
_watcher: watcher,
watched_files: self.0,
rx,
})
}
}

View File

@@ -14,9 +14,6 @@ use anyhow::Result;
pub trait Runtime { pub trait Runtime {
fn is_open(&self) -> bool; fn is_open(&self) -> bool;
fn set_timeout(&mut self, _timeout: u32) {
eprintln!("Warning: runtime doesn't support timeout");
}
fn load(&mut self, module_data: &[u8]) -> Result<()>; fn load(&mut self, module_data: &[u8]) -> Result<()>;
fn run_frame(&mut self) -> Result<()>; fn run_frame(&mut self) -> Result<()>;
} }

View File

@@ -13,8 +13,13 @@ use uw8::RunWebServer;
use uw8::Runtime; use uw8::Runtime;
fn main() -> Result<()> { fn main() -> Result<()> {
env_logger::Builder::from_env(env_logger::Env::default()).init();
let mut args = Arguments::from_env(); let mut args = Arguments::from_env();
// try to enable ansi support in win10 cmd shell
#[cfg(target_os = "windows")]
let _ = ansi_term::enable_ansi_support();
match args.subcommand()?.as_deref() { match args.subcommand()?.as_deref() {
Some("version") => { Some("version") => {
println!("{}", env!("CARGO_PKG_VERSION")); println!("{}", env!("CARGO_PKG_VERSION"));
@@ -48,6 +53,7 @@ fn main() -> Result<()> {
#[cfg(any(feature = "native", feature = "browser"))] #[cfg(any(feature = "native", feature = "browser"))]
fn run(mut args: Arguments) -> Result<()> { fn run(mut args: Arguments) -> Result<()> {
let watch_mode = args.contains(["-w", "--watch"]); let watch_mode = args.contains(["-w", "--watch"]);
#[allow(unused)]
let timeout: Option<u32> = args.opt_value_from_str(["-t", "--timeout"])?; let timeout: Option<u32> = args.opt_value_from_str(["-t", "--timeout"])?;
let mut config = Config::default(); let mut config = Config::default();
@@ -75,15 +81,21 @@ fn run(mut args: Arguments) -> Result<()> {
#[cfg(not(feature = "native"))] #[cfg(not(feature = "native"))]
let run_browser = args.contains(["-b", "--browser"]) || true; let run_browser = args.contains(["-b", "--browser"]) || true;
#[allow(unused)]
let disable_audio = args.contains(["-m", "--no-audio"]);
#[cfg(feature = "native")]
let window_config = {
let mut config = uw8_window::WindowConfig::default();
if !run_browser {
config.parse_arguments(&mut args);
}
config
};
let filename = args.free_from_os_str::<PathBuf, bool>(|s| Ok(s.into()))?; let filename = args.free_from_os_str::<PathBuf, bool>(|s| Ok(s.into()))?;
let mut watcher = uw8::FileWatcher::builder(); let mut watcher = uw8::FileWatcher::new()?;
if watch_mode {
watcher.add_file(&filename);
}
let watcher = watcher.build()?;
use std::process::exit; use std::process::exit;
@@ -91,7 +103,13 @@ fn run(mut args: Arguments) -> Result<()> {
#[cfg(not(feature = "native"))] #[cfg(not(feature = "native"))]
unimplemented!(); unimplemented!();
#[cfg(feature = "native")] #[cfg(feature = "native")]
Box::new(MicroW8::new()?) {
let mut microw8 = MicroW8::new(timeout, window_config)?;
if disable_audio {
microw8.disable_audio();
}
Box::new(microw8)
}
} else { } else {
#[cfg(not(feature = "browser"))] #[cfg(not(feature = "browser"))]
unimplemented!(); unimplemented!();
@@ -99,22 +117,23 @@ fn run(mut args: Arguments) -> Result<()> {
Box::new(RunWebServer::new()) Box::new(RunWebServer::new())
}; };
if let Some(timeout) = timeout { let mut first_run = true;
runtime.set_timeout(timeout);
}
if let Err(err) = start_cart(&filename, &mut *runtime, &config) { while runtime.is_open() {
if first_run || watcher.poll_changed_file()?.is_some() {
let (result, dependencies) = start_cart(&filename, &mut *runtime, &config);
if watch_mode {
for dep in dependencies {
watcher.add_file(dep)?;
}
}
if let Err(err) = result {
eprintln!("Load error: {}", err); eprintln!("Load error: {}", err);
if !watch_mode { if !watch_mode {
exit(1); exit(1);
} }
} }
first_run = false;
while runtime.is_open() {
if watcher.poll_changed_file()?.is_some() {
if let Err(err) = start_cart(&filename, &mut *runtime, &config) {
eprintln!("Load error: {}", err);
}
} }
if let Err(err) = runtime.run_frame() { if let Err(err) = runtime.run_frame() {
@@ -134,22 +153,33 @@ struct Config {
output_path: Option<PathBuf>, output_path: Option<PathBuf>,
} }
fn load_cart(filename: &Path, config: &Config) -> Result<Vec<u8>> { fn load_cart(filename: &Path, config: &Config) -> (Result<Vec<u8>>, Vec<PathBuf>) {
let mut dependencies = Vec::new();
fn inner(filename: &Path, config: &Config, dependencies: &mut Vec<PathBuf>) -> Result<Vec<u8>> {
let mut cart = match SourceType::of_file(filename)? {
SourceType::Binary => {
let mut cart = vec![]; let mut cart = vec![];
File::open(filename)?.read_to_end(&mut cart)?; File::open(filename)?.read_to_end(&mut cart)?;
cart
if cart[0] >= 10 {
let src = String::from_utf8(cart)?;
cart = if src.chars().find(|c| !c.is_whitespace()) == Some('(') {
wat::parse_str(src)?
} else {
curlywas::compile_str(&src, filename, curlywas::Options::default())?
};
} }
SourceType::Wat => {
let cart = wat::parse_file(filename)?;
cart
}
SourceType::CurlyWas => {
let (module, deps) = curlywas::compile_file(filename, curlywas::Options::default());
*dependencies = deps;
module?
}
};
if let Some(ref pack_config) = config.pack { if let Some(ref pack_config) = config.pack {
cart = uw8_tool::pack(&cart, pack_config)?; cart = uw8_tool::pack(&cart, pack_config)?;
println!("packed size: {} bytes", cart.len()); println!(
"\npacked size: {} bytes ({:.2})",
cart.len(),
uw8_tool::compressed_size(&cart)
);
} }
if let Some(ref path) = config.output_path { if let Some(ref path) = config.output_path {
@@ -159,15 +189,67 @@ fn load_cart(filename: &Path, config: &Config) -> Result<Vec<u8>> {
Ok(cart) Ok(cart)
} }
let result = inner(filename, config, &mut dependencies);
if dependencies.is_empty() {
dependencies.push(filename.to_path_buf());
}
(result, dependencies)
}
enum SourceType {
Binary,
Wat,
CurlyWas,
}
impl SourceType {
fn of_file(filename: &Path) -> Result<SourceType> {
if let Some(extension) = filename.extension() {
if extension == "uw8" || extension == "wasm" {
return Ok(SourceType::Binary);
} else if extension == "wat" || extension == "wast" {
return Ok(SourceType::Wat);
} else if extension == "cwa" {
return Ok(SourceType::CurlyWas);
}
}
let mut cart = vec![];
File::open(filename)?.read_to_end(&mut cart)?;
let ty = if cart[0] < 10 {
SourceType::Binary
} else {
let src = String::from_utf8(cart)?;
if src.chars().find(|&c| !c.is_whitespace() && c != ';') == Some('(') {
SourceType::Wat
} else {
SourceType::CurlyWas
}
};
Ok(ty)
}
}
#[cfg(any(feature = "native", feature = "browser"))] #[cfg(any(feature = "native", feature = "browser"))]
fn start_cart(filename: &Path, runtime: &mut dyn Runtime, config: &Config) -> Result<()> { fn start_cart(
let cart = load_cart(filename, config)?; filename: &Path,
runtime: &mut dyn Runtime,
config: &Config,
) -> (Result<()>, Vec<PathBuf>) {
let (cart, dependencies) = load_cart(filename, config);
let cart = match cart {
Ok(cart) => cart,
Err(err) => return (Err(err), dependencies),
};
if let Err(err) = runtime.load(&cart) { if let Err(err) = runtime.load(&cart) {
eprintln!("Load error: {}", err); eprintln!("Load error: {}", err);
Err(err) (Err(err), dependencies)
} else { } else {
Ok(()) (Ok(()), dependencies)
} }
} }
@@ -192,7 +274,8 @@ fn pack(mut args: Arguments) -> Result<()> {
pack: Some(pack_config), pack: Some(pack_config),
output_path: None, output_path: None,
}, },
)?; )
.0?;
File::create(out_file)?.write_all(&cart)?; File::create(out_file)?.write_all(&cart)?;
@@ -215,7 +298,7 @@ fn compile(mut args: Arguments) -> Result<()> {
let in_file = args.free_from_os_str::<PathBuf, bool>(|s| Ok(s.into()))?; let in_file = args.free_from_os_str::<PathBuf, bool>(|s| Ok(s.into()))?;
let out_file = args.free_from_os_str::<PathBuf, bool>(|s| Ok(s.into()))?; let out_file = args.free_from_os_str::<PathBuf, bool>(|s| Ok(s.into()))?;
let module = curlywas::compile_file(in_file, options)?; let module = curlywas::compile_file(in_file, options).0?;
File::create(out_file)?.write_all(&module)?; File::create(out_file)?.write_all(&module)?;
Ok(()) Ok(())

File diff suppressed because one or more lines are too long

View File

@@ -1,41 +1,34 @@
use std::sync::{Arc, Mutex}; use std::sync::{mpsc, Arc, Mutex};
use std::time::Duration; use std::time::Duration;
use std::{thread, time::Instant}; use std::{thread, time::Instant};
use anyhow::Result; use anyhow::{anyhow, Result};
use minifb::{Key, Window, WindowOptions}; use cpal::traits::*;
use rubato::Resampler;
use uw8_window::{Window, WindowConfig};
use wasmtime::{ use wasmtime::{
Engine, GlobalType, Memory, MemoryType, Module, Mutability, Store, TypedFunc, ValType, Engine, Func, GlobalType, Memory, MemoryType, Module, Mutability, Store, TypedFunc, ValType,
}; };
static GAMEPAD_KEYS: &[Key] = &[
Key::Up,
Key::Down,
Key::Left,
Key::Right,
Key::Z,
Key::X,
Key::A,
Key::S,
];
pub struct MicroW8 { pub struct MicroW8 {
window: Window,
stream: Option<cpal::Stream>,
engine: Engine, engine: Engine,
loader_module: Module, loader_module: Module,
window: Window, disable_audio: bool,
window_buffer: Vec<u32>, module_data: Option<Vec<u8>>,
instance: Option<UW8Instance>,
timeout: u32, timeout: u32,
instance: Option<UW8Instance>,
} }
struct UW8Instance { struct UW8Instance {
store: Store<()>, store: Store<()>,
memory: Memory, memory: Memory,
end_frame: TypedFunc<(), ()>, end_frame: TypedFunc<(), ()>,
update: TypedFunc<(), ()>, update: Option<TypedFunc<(), ()>>,
start_time: Instant, start_time: Instant,
module: Vec<u8>,
watchdog: Arc<Mutex<UW8WatchDog>>, watchdog: Arc<Mutex<UW8WatchDog>>,
sound_tx: Option<mpsc::SyncSender<RegisterUpdate>>,
} }
impl Drop for UW8Instance { impl Drop for UW8Instance {
@@ -47,66 +40,60 @@ impl Drop for UW8Instance {
} }
struct UW8WatchDog { struct UW8WatchDog {
interupt: wasmtime::InterruptHandle, engine: Engine,
timeout: u32,
stop: bool, stop: bool,
} }
impl MicroW8 { impl MicroW8 {
pub fn new() -> Result<MicroW8> { pub fn new(timeout: Option<u32>, window_config: WindowConfig) -> Result<MicroW8> {
let engine = wasmtime::Engine::new(wasmtime::Config::new().interruptable(true))?; let mut config = wasmtime::Config::new();
config.cranelift_opt_level(wasmtime::OptLevel::Speed);
if timeout.is_some() {
config.epoch_interruption(true);
}
let engine = wasmtime::Engine::new(&config)?;
let loader_module = let loader_module =
wasmtime::Module::new(&engine, include_bytes!("../platform/bin/loader.wasm"))?; wasmtime::Module::new(&engine, include_bytes!("../platform/bin/loader.wasm"))?;
let options = WindowOptions { let window = Window::new(window_config)?;
scale: minifb::Scale::X2,
scale_mode: minifb::ScaleMode::AspectRatioStretch,
resize: true,
..Default::default()
};
let mut window = Window::new("MicroW8", 320, 240, options)?;
window.limit_update_rate(Some(std::time::Duration::from_micros(16666)));
Ok(MicroW8 { Ok(MicroW8 {
window,
stream: None,
engine, engine,
loader_module, loader_module,
window, disable_audio: false,
window_buffer: vec![0u32; 320 * 240], module_data: None,
timeout: timeout.unwrap_or(0),
instance: None, instance: None,
timeout: 30,
}) })
} }
fn reset(&mut self) { pub fn disable_audio(&mut self) {
self.instance = None; self.disable_audio = true;
for v in &mut self.window_buffer {
*v = 0;
}
} }
} }
impl super::Runtime for MicroW8 { impl super::Runtime for MicroW8 {
fn is_open(&self) -> bool { fn is_open(&self) -> bool {
self.window.is_open() && !self.window.is_key_down(Key::Escape) self.window.is_open()
}
fn set_timeout(&mut self, timeout: u32) {
self.timeout = timeout;
} }
fn load(&mut self, module_data: &[u8]) -> Result<()> { fn load(&mut self, module_data: &[u8]) -> Result<()> {
self.reset(); self.stream = None;
self.instance = None;
let mut store = wasmtime::Store::new(&self.engine, ()); let mut store = wasmtime::Store::new(&self.engine, ());
store.set_epoch_deadline(60);
let memory = wasmtime::Memory::new(&mut store, MemoryType::new(4, Some(4)))?; let memory = wasmtime::Memory::new(&mut store, MemoryType::new(4, Some(4)))?;
let mut linker = wasmtime::Linker::new(&self.engine); let mut linker = wasmtime::Linker::new(&self.engine);
linker.define("env", "memory", memory)?; linker.define(&store, "env", "memory", memory)?;
let loader_instance = linker.instantiate(&mut store, &self.loader_module)?; let loader_instance = linker.instantiate(&mut store, &self.loader_module)?;
let load_uw8 = loader_instance.get_typed_func::<i32, i32, _>(&mut store, "load_uw8")?; let load_uw8 = loader_instance.get_typed_func::<i32, i32>(&mut store, "load_uw8")?;
let platform_data = include_bytes!("../platform/bin/platform.uw8"); let platform_data = include_bytes!("../platform/bin/platform.uw8");
memory.data_mut(&mut store)[..platform_data.len()].copy_from_slice(platform_data); memory.data_mut(&mut store)[..platform_data.len()].copy_from_slice(platform_data);
@@ -119,6 +106,131 @@ impl super::Runtime for MicroW8 {
let module_length = load_uw8.call(&mut store, module_data.len() as i32)? as u32 as usize; let module_length = load_uw8.call(&mut store, module_data.len() as i32)? as u32 as usize;
let module = wasmtime::Module::new(&self.engine, &memory.data(&store)[..module_length])?; let module = wasmtime::Module::new(&self.engine, &memory.data(&store)[..module_length])?;
add_native_functions(&mut linker, &mut store)?;
let platform_instance = instantiate_platform(&mut linker, &mut store, &platform_module)?;
let watchdog = Arc::new(Mutex::new(UW8WatchDog {
engine: self.engine.clone(),
stop: false,
}));
{
let watchdog = watchdog.clone();
thread::spawn(move || loop {
thread::sleep(Duration::from_millis(17));
if let Ok(watchdog) = watchdog.lock() {
if watchdog.stop {
break;
}
watchdog.engine.increment_epoch();
} else {
break;
}
});
}
let instance = linker.instantiate(&mut store, &module)?;
let end_frame = platform_instance.get_typed_func::<(), ()>(&mut store, "endFrame")?;
let update = instance.get_typed_func::<(), ()>(&mut store, "upd").ok();
if let Some(start) = instance.get_typed_func::<(), ()>(&mut store, "start").ok() {
start.call(&mut store, ())?;
}
let (sound_tx, stream) = if self.disable_audio {
(None, None)
} else {
match init_sound(&self.engine, &platform_module, &module) {
Ok(sound) => {
sound.stream.play()?;
(Some(sound.tx), Some(sound.stream))
}
Err(err) => {
eprintln!("Failed to init sound: {}", err);
(None, None)
}
}
};
self.instance = Some(UW8Instance {
store,
memory,
end_frame,
update,
start_time: Instant::now(),
watchdog,
sound_tx,
});
self.stream = stream;
self.module_data = Some(module_data.into());
Ok(())
}
fn run_frame(&mut self) -> Result<()> {
let input = self.window.begin_frame();
if input.reset {
if let Some(module_data) = self.module_data.take() {
self.load(&module_data)?;
}
}
let now = Instant::now();
let mut result = Ok(());
if let Some(mut instance) = self.instance.take() {
let time = (now - instance.start_time).as_millis() as i32;
let next_frame = {
let offset = ((time as u32 as i64 * 6) % 100 - 50) / 6;
let max = now + Duration::from_millis(17);
let next_center = now + Duration::from_millis((16 - offset) as u64);
next_center.min(max)
};
{
let mem = instance.memory.data_mut(&mut instance.store);
mem[64..68].copy_from_slice(&time.to_le_bytes());
mem[68..72].copy_from_slice(&input.gamepads);
}
instance.store.set_epoch_deadline(self.timeout as u64);
if let Some(ref update) = instance.update {
if let Err(err) = update.call(&mut instance.store, ()) {
result = Err(err);
}
}
instance.end_frame.call(&mut instance.store, ())?;
let memory = instance.memory.data(&instance.store);
let mut sound_regs = [0u8; 32];
sound_regs.copy_from_slice(&memory[80..112]);
if let Some(ref sound_tx) = instance.sound_tx {
let _ = sound_tx.send(RegisterUpdate {
time,
data: sound_regs,
});
}
let framebuffer_mem = &memory[120..(120 + 320 * 240)];
let palette_mem = &memory[0x13000..];
self.window
.end_frame(framebuffer_mem, palette_mem, next_frame);
if result.is_ok() {
self.instance = Some(instance);
}
}
result?;
Ok(())
}
}
fn add_native_functions(
linker: &mut wasmtime::Linker<()>,
store: &mut wasmtime::Store<()>,
) -> Result<()> {
linker.func_wrap("env", "acos", |v: f32| v.acos())?; linker.func_wrap("env", "acos", |v: f32| v.acos())?;
linker.func_wrap("env", "asin", |v: f32| v.asin())?; linker.func_wrap("env", "asin", |v: f32| v.asin())?;
linker.func_wrap("env", "atan", |v: f32| v.atan())?; linker.func_wrap("env", "atan", |v: f32| v.atan())?;
@@ -129,132 +241,280 @@ impl super::Runtime for MicroW8 {
linker.func_wrap("env", "sin", |v: f32| v.sin())?; linker.func_wrap("env", "sin", |v: f32| v.sin())?;
linker.func_wrap("env", "tan", |v: f32| v.tan())?; linker.func_wrap("env", "tan", |v: f32| v.tan())?;
linker.func_wrap("env", "pow", |a: f32, b: f32| a.powf(b))?; linker.func_wrap("env", "pow", |a: f32, b: f32| a.powf(b))?;
for i in 9..64 { for i in 10..64 {
linker.func_wrap("env", &format!("reserved{}", i), || {})?; linker.func_wrap("env", &format!("reserved{}", i), || {})?;
} }
let log_line = std::sync::Mutex::new(String::new());
linker.func_wrap("env", "logChar", move |c: i32| {
let mut log_line = log_line.lock().unwrap();
if c == 10 {
println!("{}", log_line);
log_line.clear();
} else {
log_line.push(c as u8 as char);
}
})?;
for i in 0..16 { for i in 0..16 {
linker.define( let global = wasmtime::Global::new(
"env", &mut *store,
&format!("g_reserved{}", i),
wasmtime::Global::new(
&mut store,
GlobalType::new(ValType::I32, Mutability::Const), GlobalType::new(ValType::I32, Mutability::Const),
0.into(), 0.into(),
)?,
)?; )?;
linker.define(&store, "env", &format!("g_reserved{}", i), global)?;
} }
let platform_instance = linker.instantiate(&mut store, &platform_module)?; Ok(())
}
for export in platform_instance.exports(&mut store) { fn instantiate_platform(
linker.define( linker: &mut wasmtime::Linker<()>,
"env", store: &mut wasmtime::Store<()>,
export.name(), platform_module: &wasmtime::Module,
export ) -> Result<wasmtime::Instance> {
.into_func() let platform_instance = linker.instantiate(&mut *store, &platform_module)?;
let exports: Vec<(String, Func)> = platform_instance
.exports(&mut *store)
.map(|e| {
(
e.name().to_owned(),
e.into_func()
.expect("platform surely only exports functions"), .expect("platform surely only exports functions"),
)?; )
})
.collect();
for (name, func) in exports {
linker.define(&store, "env", &name, func)?;
} }
let watchdog = Arc::new(Mutex::new(UW8WatchDog { Ok(platform_instance)
interupt: store.interrupt_handle()?, }
timeout: self.timeout,
stop: false, struct RegisterUpdate {
})); time: i32,
data: [u8; 32],
}
struct Uw8Sound {
stream: cpal::Stream,
tx: mpsc::SyncSender<RegisterUpdate>,
}
fn init_sound(
engine: &wasmtime::Engine,
platform_module: &wasmtime::Module,
module: &wasmtime::Module,
) -> Result<Uw8Sound> {
let mut store = wasmtime::Store::new(engine, ());
store.set_epoch_deadline(60);
let memory = wasmtime::Memory::new(&mut store, MemoryType::new(4, Some(4)))?;
let mut linker = wasmtime::Linker::new(engine);
linker.define(&store, "env", "memory", memory)?;
add_native_functions(&mut linker, &mut store)?;
let platform_instance = instantiate_platform(&mut linker, &mut store, platform_module)?;
let instance = linker.instantiate(&mut store, module)?;
let snd = instance
.get_typed_func::<(i32,), f32>(&mut store, "snd")
.or_else(|_| platform_instance.get_typed_func::<(i32,), f32>(&mut store, "sndGes"))?;
let host = cpal::default_host();
let device = host
.default_output_device()
.ok_or_else(|| anyhow!("No audio output device available"))?;
let mut configs: Vec<_> = device
.supported_output_configs()?
.filter(|config| {
config.channels() == 2
&& (config.sample_format() == cpal::SampleFormat::F32
|| config.sample_format() == cpal::SampleFormat::I16)
})
.collect();
configs.sort_by_key(|config| {
let rate = 44100
.max(config.min_sample_rate().0)
.min(config.max_sample_rate().0);
let prio = if rate >= 44100 {
rate - 44100
} else {
(44100 - rate) * 1000
};
prio + (config.sample_format() == cpal::SampleFormat::I16) as u32
});
let config = configs
.into_iter()
.next()
.ok_or_else(|| anyhow!("Could not find float or 16bit signed output config"))?;
let sample_rate = cpal::SampleRate(44100)
.max(config.min_sample_rate())
.min(config.max_sample_rate());
let config = config.with_sample_rate(sample_rate);
let buffer_size = match *config.buffer_size() {
cpal::SupportedBufferSize::Unknown => cpal::BufferSize::Default,
cpal::SupportedBufferSize::Range { min, max } => {
cpal::BufferSize::Fixed(256.max(min).min(max))
}
};
let sample_format = config.sample_format();
let config = cpal::StreamConfig {
buffer_size,
..config.config()
};
let sample_rate = config.sample_rate.0 as usize;
let (tx, rx) = mpsc::sync_channel::<RegisterUpdate>(30);
struct Resampler {
resampler: rubato::FftFixedIn<f32>,
input_buffers: Vec<Vec<f32>>,
output_buffers: Vec<Vec<f32>>,
output_index: usize,
}
let mut resampler: Option<Resampler> = if sample_rate == 44100 {
None
} else {
let rs = rubato::FftFixedIn::new(44100, sample_rate, 128, 1, 2)?;
let input_buffers = rs.input_buffer_allocate(true);
let output_buffers = rs.output_buffer_allocate(true);
Some(Resampler {
resampler: rs,
input_buffers,
output_buffers,
output_index: usize::MAX,
})
};
let mut sample_index = 0;
let mut pending_updates: Vec<RegisterUpdate> = Vec::with_capacity(30);
let mut current_time = 0;
let mut callback = move |mut outer_buffer: &mut [f32], _: &_| {
let mut first_update = true;
while let Ok(update) = rx.try_recv() {
if first_update {
current_time += update.time.wrapping_sub(current_time) / 8;
first_update = false;
}
pending_updates.push(update);
}
while !outer_buffer.is_empty() {
store.set_epoch_deadline(30);
while pending_updates
.first()
.into_iter()
.any(|u| u.time.wrapping_sub(current_time) <= 0)
{
let update = pending_updates.remove(0);
memory.write(&mut store, 80, &update.data).unwrap();
}
let duration = if let Some(update) = pending_updates.first() {
((update.time.wrapping_sub(current_time) as usize) * sample_rate + 999) / 1000
} else {
outer_buffer.len()
};
let step_size = (duration.max(64) * 2).min(outer_buffer.len());
let mut buffer = &mut outer_buffer[..step_size];
{ {
let watchdog = watchdog.clone(); let mem = memory.data_mut(&mut store);
thread::spawn(move || loop { mem[64..68].copy_from_slice(&current_time.to_le_bytes());
thread::sleep(Duration::from_millis(17));
if let Ok(mut watchdog) = watchdog.lock() {
if watchdog.stop {
break;
} }
if watchdog.timeout > 0 {
watchdog.timeout -= 1; fn clamp_sample(s: f32) -> f32 {
if watchdog.timeout == 0 { if s.is_nan() {
watchdog.interupt.interrupt(); 0.0
} else {
s.max(-1.0).min(1.0)
}
}
if let Some(ref mut resampler) = resampler {
while !buffer.is_empty() {
let copy_size = resampler.output_buffers[0]
.len()
.saturating_sub(resampler.output_index)
.min(buffer.len() / 2);
if copy_size == 0 {
resampler.input_buffers[0].clear();
resampler.input_buffers[1].clear();
for _ in 0..resampler.resampler.input_frames_next() {
resampler.input_buffers[0].push(clamp_sample(
snd.call(&mut store, (sample_index,)).unwrap_or(0.0),
));
resampler.input_buffers[1].push(clamp_sample(
snd.call(&mut store, (sample_index + 1,)).unwrap_or(0.0),
));
sample_index = sample_index.wrapping_add(2);
}
resampler
.resampler
.process_into_buffer(
&resampler.input_buffers,
&mut resampler.output_buffers,
None,
)
.unwrap();
resampler.output_index = 0;
} else {
for i in 0..copy_size {
buffer[i * 2] = resampler.output_buffers[0][resampler.output_index + i];
buffer[i * 2 + 1] =
resampler.output_buffers[1][resampler.output_index + i];
}
resampler.output_index += copy_size;
buffer = &mut buffer[copy_size * 2..];
} }
} }
} else { } else {
break; for v in buffer {
} *v = clamp_sample(snd.call(&mut store, (sample_index,)).unwrap_or(0.0));
}); sample_index = sample_index.wrapping_add(1);
}
let instance = linker.instantiate(&mut store, &module)?;
if let Ok(mut watchdog) = watchdog.lock() {
watchdog.timeout = 0;
}
let end_frame = platform_instance.get_typed_func::<(), (), _>(&mut store, "endFrame")?;
let update = instance.get_typed_func::<(), (), _>(&mut store, "upd")?;
self.instance = Some(UW8Instance {
store,
memory,
end_frame,
update,
start_time: Instant::now(),
module: module_data.into(),
watchdog,
});
Ok(())
}
fn run_frame(&mut self) -> Result<()> {
let mut result = Ok(());
if let Some(mut instance) = self.instance.take() {
{
let time = instance.start_time.elapsed().as_millis() as i32;
let mut gamepad: u32 = 0;
for key in self.window.get_keys() {
if let Some(index) = GAMEPAD_KEYS
.iter()
.enumerate()
.find(|(_, &k)| k == key)
.map(|(i, _)| i)
{
gamepad |= 1 << index;
} }
} }
let mem = instance.memory.data_mut(&mut instance.store); outer_buffer = &mut outer_buffer[step_size..];
mem[64..68].copy_from_slice(&time.to_le_bytes()); current_time = current_time.wrapping_add((step_size * 500 / sample_rate).max(1) as i32);
mem[68..72].copy_from_slice(&gamepad.to_le_bytes());
} }
};
let stream = if sample_format == cpal::SampleFormat::F32 {
device.build_output_stream(
&config,
callback,
move |err| {
dbg!(err);
},
None,
)?
} else {
device.build_output_stream(
&config,
move |mut buffer: &mut [i16], info| {
let mut float_buffer = [0f32; 256];
if let Ok(mut watchdog) = instance.watchdog.lock() { while !buffer.is_empty() {
watchdog.timeout = self.timeout; let step_size = buffer.len().min(float_buffer.len());
let step_buffer = &mut float_buffer[..step_size];
callback(step_buffer, info);
for (dest, src) in buffer.iter_mut().take(step_size).zip(step_buffer.iter()) {
*dest = (src.max(-1.0).min(1.0) * 32767.0) as i16;
} }
result = instance.update.call(&mut instance.store, ()); buffer = &mut buffer[step_size..];
if let Ok(mut watchdog) = instance.watchdog.lock() {
watchdog.timeout = 0;
} }
instance.end_frame.call(&mut instance.store, ())?; },
move |err| {
dbg!(err);
},
None,
)?
};
let memory = instance.memory.data(&instance.store); Ok(Uw8Sound { stream, tx })
let framebuffer = &memory[120..(120 + 320 * 240)];
let palette = &memory[0x13000..];
for (i, &color_index) in framebuffer.iter().enumerate() {
let offset = color_index as usize * 4;
self.window_buffer[i] = 0xff000000
| ((palette[offset] as u32) << 16)
| ((palette[offset + 1] as u32) << 8)
| palette[offset + 2] as u32;
}
if self.window.is_key_pressed(Key::R, minifb::KeyRepeat::No) {
self.load(&instance.module)?;
} else if result.is_ok() {
self.instance = Some(instance);
}
}
self.window
.update_with_buffer(&self.window_buffer, 320, 240)?;
result?;
Ok(())
}
} }

View File

@@ -11,6 +11,7 @@ use warp::{http::Response, Filter};
pub struct RunWebServer { pub struct RunWebServer {
cart: Arc<Mutex<Vec<u8>>>, cart: Arc<Mutex<Vec<u8>>>,
tx: broadcast::Sender<()>, tx: broadcast::Sender<()>,
socket_addr: SocketAddr,
} }
impl RunWebServer { impl RunWebServer {
@@ -18,8 +19,13 @@ impl RunWebServer {
let cart = Arc::new(Mutex::new(Vec::new())); let cart = Arc::new(Mutex::new(Vec::new()));
let (tx, _) = broadcast::channel(1); let (tx, _) = broadcast::channel(1);
let socket_addr = "127.0.0.1:3030"
.parse::<SocketAddr>()
.expect("Failed to parse socket address");
let server_cart = cart.clone(); let server_cart = cart.clone();
let server_tx = tx.clone(); let server_tx = tx.clone();
let server_addr = socket_addr.clone();
thread::spawn(move || { thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread() let rt = tokio::runtime::Builder::new_current_thread()
.enable_io() .enable_io()
@@ -47,24 +53,26 @@ impl RunWebServer {
warp::sse::reply(warp::sse::keep_alive().stream(event_stream(&server_tx))) warp::sse::reply(warp::sse::keep_alive().stream(event_stream(&server_tx)))
}); });
let socket_addr = "127.0.0.1:3030" let server_future = warp::serve(html.or(cart).or(events)).bind(server_addr);
.parse::<SocketAddr>()
.expect("Failed to parse socket address");
let server_future = warp::serve(html.or(cart).or(events)).bind(socket_addr);
println!("Point browser at http://{}", socket_addr);
let _ignore_result = webbrowser::open(&format!("http://{}", socket_addr));
server_future.await server_future.await
}); });
}); });
RunWebServer { cart, tx } RunWebServer {
cart,
tx,
socket_addr,
}
} }
} }
impl super::Runtime for RunWebServer { impl super::Runtime for RunWebServer {
fn load(&mut self, module_data: &[u8]) -> Result<()> { fn load(&mut self, module_data: &[u8]) -> Result<()> {
if let Ok(mut lock) = self.cart.lock() { if let Ok(mut lock) = self.cart.lock() {
if lock.is_empty() && !module_data.is_empty() {
println!("Point browser at http://{}", self.socket_addr);
let _ignore_result = webbrowser::open(&format!("http://{}", self.socket_addr));
}
lock.clear(); lock.clear();
lock.extend_from_slice(module_data); lock.extend_from_slice(module_data);
} }

View File

@@ -1,13 +0,0 @@
import "env.memory" memory(4);
import "env.printString" fn print(i32);
export fn upd() {
}
start fn start() {
print(0);
}
data 0 {
"Press " i8(0xe0) " and " i8(0xe1) " to adjust, " i8(0xcc) " to commit." i8(0)
}

BIN
test.wasm

Binary file not shown.

62
test/drawing_test.cwa Normal file
View File

@@ -0,0 +1,62 @@
include "../examples/include/microw8-api.cwa"
global mut counter = 0;
export fn upd() {
cls(0);
let col: i32 = 1;
loop colors {
if !testCircle(counter, col) {
printInt(counter);
return;
}
counter += 1;
branch_if (col +:= 1) < 256: colors;
}
}
fn testCircle(seed: i32, col: i32) -> i32 {
randomSeed(seed);
let cx = randomf() * 640_f - 160_f;
let cy = randomf() * 480_f - 120_f;
let radius = randomf() * 4_f;
radius *= radius;
radius *= radius;
circle(cx, cy, radius, col);
let min_x = max(0_f, floor(cx - radius - 1_f)) as i32;
let min_y = max(0_f, floor(cy - radius - 1_f)) as i32;
let max_x = min(320_f, ceil(cx + radius + 1_f)) as i32;
let max_y = min(240_f, ceil(cy + radius + 1_f)) as i32;
let x = min_x;
loop xloop {
if x < max_x {
let y = min_y;
loop yloop {
if y < max_y {
let rx = x as f32 + 0.5 - cx;
let ry = y as f32 + 0.5 - cy;
let d = sqrt(rx*rx + ry*ry) - radius;
if abs(d) > 0.001 {
let is_inside = d < 0_f;
let is_plotted = getPixel(x, y) == col;
if is_inside != is_plotted {
return 0;
}
}
y += 1;
branch yloop;
}
}
x += 1;
branch xloop;
}
}
1
}

20
test/frame_time.cwa Normal file
View File

@@ -0,0 +1,20 @@
include "../examples/include/microw8-api.cwa"
global mut pos = 0;
global mut next = 0;
export fn upd() {
let lazy t = 32!32;
let lazy tick = t * 6 / 100;
let lazy rel = t - tick * 100 / 6;
setBackgroundColor(select(tick == next, 0, select(tick < next, 0x35, 0x55)));
setCursorPosition(pos % 13 * 3, pos / 13 % 30);
if rel < 10 {
printChar(32);
}
printInt(rel);
pos = pos + 1;
next = tick + 1;
}

31
test/ges_test.cwa Normal file
View File

@@ -0,0 +1,31 @@
import "env.memory" memory(4);
import "env.pow" fn pow(f32, f32) -> f32;
import "env.sin" fn sin(f32) -> f32;
import "env.cls" fn cls(i32);
import "env.exp" fn exp(f32) -> f32;
import "env.rectangle" fn rectangle(f32, f32, f32, f32, i32);
include "../platform/src/ges.cwa"
export fn snd(t: i32) -> f32 {
sndGes(t)
}
export fn upd() {
80?0 = 32!32 / 200 & 2 | 0x41;
80?3 = (32!32 / 400)%8*12/7 + 40;
let pulse = (32!32 * 256 / 2000) & 511;
if pulse >= 256 {
pulse = 511 - pulse;
}
80?1 = pulse;
cls(0);
rectangle(0.0, 100.0, (pulse * 320 / 256) as f32, 16.0, 15);
}
data 80 {
i8(
0x41, 0, 0, 80, 0x70, 0
)
}

7
test/log.cwa Normal file
View File

@@ -0,0 +1,7 @@
include "../examples/include/microw8-api.cwa"
export fn upd() {
printChar('\06f: ');
printInt(32!32 * 6 / 100);
printChar('\n\4');
}

59
test/plot_ges.cwa Normal file
View File

@@ -0,0 +1,59 @@
include "../examples/include/microw8-api.cwa"
export fn upd() {
80?0 = (32!32 >> 11 << 6) | 5;
80?1 = (sin(time() * 6 as f32) * 95 as f32) as i32 + 128;
plotGes();
}
data 80 { i8 (
1, 128, 0, 69, 0, 15,
0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
0xff, 0xff,
0xc1, 0, 0, 110, 0, 0
) }
//import "env.gesSnd" fn gesSnd(i32) -> f32;
include "../platform/src/ges.cwa"
export fn snd(t: i32) -> f32 {
gesSnd(t)
}
global mut samplePos: i32 = 0;
const SoundBuffer = 0x30000;
fn plotGes() {
rectangle(0 as f32, 10 as f32, 320 as f32, 320 as f32, 0);
let count = (time() * 44100 as f32) as i32 * 2 - samplePos;
let i: i32;
loop genLoop {
(i*4)$SoundBuffer = gesSnd(samplePos + i);
branch_if (i := i + 1) < count: genLoop;
}
samplePos = samplePos + count;
let ch: i32;
loop channelLoop {
let offset = 159;
i = 0;
loop searchLoop {
offset = offset + 1;
branch_if (offset * 8 + ch - 8)$SoundBuffer < 0 as f32 | (offset * 8 + ch)$SoundBuffer >= 0 as f32 & offset + 160 < count: searchLoop;
}
offset = ch + (offset - 160) * 8;
i = 0;
loop plotLoop {
setPixel(i, floor((i * 8 + offset)$SoundBuffer * 127 as f32) as i32 + 60 + ch * (120/8), 15);
branch_if (i := i + 1) < 320: plotLoop;
}
branch_if (ch := ch + 8) < 16: channelLoop;
}
}

5
test/start_fn.cwa Normal file
View File

@@ -0,0 +1,5 @@
include "../examples/include/microw8-api.cwa"
export fn start() {
printChar('Test');
}

13
test/text_modes.cwa Normal file
View File

@@ -0,0 +1,13 @@
include "../examples/include/microw8-api.cwa"
export fn upd() {
printString(USER_MEM);
}
data USER_MEM {
i8(12, 31, 5, 6) "Text mode"
i8(5, 31, 4, 5) "Graphics mode"
i8(6) "Console output\nSecond line\n"
i8(4, 31, 4, 12) "Back to text mode"
i8(0)
}

1
todo.txt Normal file
View File

@@ -0,0 +1 @@
* add support for 16bit sound (not just float)

136
uw8-tool/Cargo.lock generated
View File

@@ -56,6 +56,21 @@ dependencies = [
"lazy_static", "lazy_static",
] ]
[[package]]
name = "form_urlencoded"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
dependencies = [
"percent-encoding",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.3.3" version = "0.3.3"
@@ -71,6 +86,26 @@ version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005"
[[package]]
name = "idna"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
dependencies = [
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "indexmap"
version = "1.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.4.0" version = "1.4.0"
@@ -83,6 +118,12 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
[[package]]
name = "lexopt"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478ee9e62aaeaf5b140bd4138753d1f109765488581444218d3ddda43234f3e8"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.112" version = "0.2.112"
@@ -120,10 +161,16 @@ dependencies = [
] ]
[[package]] [[package]]
name = "pico-args" name = "percent-encoding"
version = "0.4.2" version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468" checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
name = "pico-args"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
@@ -163,6 +210,26 @@ dependencies = [
"unicode-xid", "unicode-xid",
] ]
[[package]]
name = "thiserror"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "time" name = "time"
version = "0.1.44" version = "0.1.44"
@@ -174,6 +241,36 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "tinyvec"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "unicode-bidi"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58"
[[package]]
name = "unicode-normalization"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
dependencies = [
"tinyvec",
]
[[package]] [[package]]
name = "unicode-segmentation" name = "unicode-segmentation"
version = "1.8.0" version = "1.8.0"
@@ -188,13 +285,24 @@ checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]] [[package]]
name = "upkr" name = "upkr"
version = "0.1.0" version = "0.2.1"
source = "git+https://github.com/exoticorn/upkr.git?rev=2e7983fc#2e7983fc650788d98da2eecef2d16f63e849e4a0" source = "git+https://github.com/exoticorn/upkr.git?rev=080db40d0088bbee2bdf3c5c75288ac7853d6b7a#080db40d0088bbee2bdf3c5c75288ac7853d6b7a"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cdivsufsort", "cdivsufsort",
"pbr", "lexopt",
"pico-args", "thiserror",
]
[[package]]
name = "url"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
] ]
[[package]] [[package]]
@@ -207,7 +315,7 @@ dependencies = [
"upkr", "upkr",
"walrus", "walrus",
"wasm-encoder", "wasm-encoder",
"wasmparser 0.81.0", "wasmparser 0.99.0",
] ]
[[package]] [[package]]
@@ -244,9 +352,9 @@ checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]] [[package]]
name = "wasm-encoder" name = "wasm-encoder"
version = "0.8.0" version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db0c351632e46cc06a58a696a6c11e4cf90cad4b9f8f07a0b59128d616c29bb0" checksum = "ef126be0e14bdf355ac1a8b41afc89195289e5c7179f80118e3abddb472f0810"
dependencies = [ dependencies = [
"leb128", "leb128",
] ]
@@ -259,9 +367,13 @@ checksum = "b35c86d22e720a07d954ebbed772d01180501afe7d03d464f413bb5f8914a8d6"
[[package]] [[package]]
name = "wasmparser" name = "wasmparser"
version = "0.81.0" version = "0.99.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98930446519f63d00a836efdc22f67766ceae8dbcc1571379f2bcabc6b2b9abc" checksum = "9ef3b717afc67f848f412d4f02c127dd3e35a0eecd58c684580414df4fde01d3"
dependencies = [
"indexmap",
"url",
]
[[package]] [[package]]
name = "winapi" name = "winapi"

View File

@@ -6,10 +6,10 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
wasmparser = "0.81" wasmparser = "0.99"
wasm-encoder = "0.8" wasm-encoder = "0.22"
walrus = "0.19" walrus = "0.19"
anyhow = "1" anyhow = "1"
pico-args = "0.4" pico-args = "0.5"
upkr = { git = "https://github.com/exoticorn/upkr.git", rev = "2e7983fc" } upkr = { git = "https://github.com/exoticorn/upkr.git", rev = "080db40d0088bbee2bdf3c5c75288ac7853d6b7a" }
pbr = "1" pbr = "1"

View File

@@ -3,7 +3,7 @@ use std::{collections::HashMap, fs::File, path::Path};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use std::io::prelude::*; use std::io::prelude::*;
use wasm_encoder::{ use wasm_encoder::{
CodeSection, EntityType, Export, ExportSection, Function, FunctionSection, ImportSection, CodeSection, EntityType, ExportKind, ExportSection, Function, FunctionSection, ImportSection,
Instruction, MemoryType, Module, TypeSection, ValType, Instruction, MemoryType, Module, TypeSection, ValType,
}; };
use ValType::*; use ValType::*;
@@ -71,26 +71,118 @@ impl BaseModule {
add_function(&mut functions, &type_map, "randomSeed", &[I32], None); add_function(&mut functions, &type_map, "randomSeed", &[I32], None);
add_function(&mut functions, &type_map, "cls", &[I32], None); add_function(&mut functions, &type_map, "cls", &[I32], None);
add_function(&mut functions, &type_map, "setPixel", &[I32, I32, I32], None); add_function(
add_function(&mut functions, &type_map, "getPixel", &[I32, I32], Some(I32)); &mut functions,
add_function(&mut functions, &type_map, "hline", &[I32, I32, I32, I32], None); &type_map,
add_function(&mut functions, &type_map, "rectangle", &[F32, F32, F32, F32, I32], None); "setPixel",
add_function(&mut functions, &type_map, "circle", &[F32, F32, F32, I32], None); &[I32, I32, I32],
add_function(&mut functions, &type_map, "line", &[F32, F32, F32, F32, I32], None); None,
);
add_function(
&mut functions,
&type_map,
"getPixel",
&[I32, I32],
Some(I32),
);
add_function(
&mut functions,
&type_map,
"hline",
&[I32, I32, I32, I32],
None,
);
add_function(
&mut functions,
&type_map,
"rectangle",
&[F32, F32, F32, F32, I32],
None,
);
add_function(
&mut functions,
&type_map,
"circle",
&[F32, F32, F32, I32],
None,
);
add_function(
&mut functions,
&type_map,
"line",
&[F32, F32, F32, F32, I32],
None,
);
add_function(&mut functions, &type_map, "time", &[], Some(F32)); add_function(&mut functions, &type_map, "time", &[], Some(F32));
add_function(&mut functions, &type_map, "isButtonPressed", &[I32], Some(I32)); add_function(
add_function(&mut functions, &type_map, "isButtonTriggered", &[I32], Some(I32)); &mut functions,
&type_map,
"isButtonPressed",
&[I32],
Some(I32),
);
add_function(
&mut functions,
&type_map,
"isButtonTriggered",
&[I32],
Some(I32),
);
add_function(&mut functions, &type_map, "printChar", &[I32], None); add_function(&mut functions, &type_map, "printChar", &[I32], None);
add_function(&mut functions, &type_map, "printString", &[I32], None); add_function(&mut functions, &type_map, "printString", &[I32], None);
add_function(&mut functions, &type_map, "printInt", &[I32], None); add_function(&mut functions, &type_map, "printInt", &[I32], None);
add_function(&mut functions, &type_map, "setTextColor", &[I32], None); add_function(&mut functions, &type_map, "setTextColor", &[I32], None);
add_function(&mut functions, &type_map, "setBackgroundColor", &[I32], None); add_function(
add_function(&mut functions, &type_map, "setCursorPosition", &[I32, I32], None); &mut functions,
&type_map,
"setBackgroundColor",
&[I32],
None,
);
add_function(
&mut functions,
&type_map,
"setCursorPosition",
&[I32, I32],
None,
);
add_function(&mut functions, &type_map, "rectangle_outline", &[F32, F32, F32, F32, I32], None); add_function(
add_function(&mut functions, &type_map, "circle_outline", &[F32, F32, F32, I32], None); &mut functions,
&type_map,
"rectangleOutline",
&[F32, F32, F32, F32, I32],
None,
);
add_function(
&mut functions,
&type_map,
"circleOutline",
&[F32, F32, F32, I32],
None,
);
add_function(&mut functions, &type_map, "exp", &[F32], Some(F32));
add_function(&mut functions, &type_map, "playNote", &[I32, I32], None);
add_function(&mut functions, &type_map, "sndGes", &[I32], Some(F32));
add_function(
&mut functions,
&type_map,
"blitSprite",
&[I32, I32, I32, I32, I32],
None,
);
add_function(
&mut functions,
&type_map,
"grabSprite",
&[I32, I32, I32, I32, I32],
None,
);
for i in functions.len()..64 { for i in functions.len()..64 {
add_function( add_function(
@@ -141,13 +233,13 @@ impl BaseModule {
let mut imports = ImportSection::new(); let mut imports = ImportSection::new();
for (module, name, type_) in &self.function_imports { for (module, name, type_) in &self.function_imports {
imports.import(*module, Some(name.as_str()), EntityType::Function(*type_)); imports.import(*module, name.as_str(), EntityType::Function(*type_));
} }
for (module, name, import) in &self.global_imports { for (module, name, import) in &self.global_imports {
imports.import( imports.import(
*module, *module,
Some(name.as_str()), name.as_str(),
EntityType::Global(wasm_encoder::GlobalType { EntityType::Global(wasm_encoder::GlobalType {
val_type: import.type_, val_type: import.type_,
mutable: import.mutable, mutable: import.mutable,
@@ -157,11 +249,12 @@ impl BaseModule {
imports.import( imports.import(
"env", "env",
Some("memory"), "memory",
MemoryType { MemoryType {
minimum: self.memory as u64, minimum: self.memory as u64,
maximum: None, maximum: None,
memory64: false, memory64: false,
shared: false,
}, },
); );
@@ -182,7 +275,7 @@ impl BaseModule {
let mut exports = ExportSection::new(); let mut exports = ExportSection::new();
for (name, fnc) in &self.exports { for (name, fnc) in &self.exports {
exports.export(*name, Export::Function(*fnc)); exports.export(*name, ExportKind::Func, *fnc);
} }
module.section(&exports); module.section(&exports);
@@ -210,10 +303,75 @@ impl BaseModule {
pub fn create_binary(path: &Path) -> Result<()> { pub fn create_binary(path: &Path) -> Result<()> {
let base1 = BaseModule::for_format_version(1)?.to_wasm(); let base1 = BaseModule::for_format_version(1)?.to_wasm();
let data = upkr::pack(&base1, 4, None); let data = upkr::pack(&base1, 4, &upkr::Config::default(), None);
File::create(path)?.write_all(&data)?; File::create(path)?.write_all(&data)?;
Ok(()) Ok(())
} }
pub fn write_as_cwa<P: AsRef<Path>>(&self, path: P) -> Result<()> {
fn inner(mut file: File, base: &BaseModule) -> Result<()> {
writeln!(
file,
"// MicroW8 APIs, to be `include`d in CurlyWas sources"
)?;
writeln!(file, "import \"env.memory\" memory({});", base.memory)?;
writeln!(file)?;
for &(module, ref name, type_id) in &base.function_imports {
if !name.contains("reserved") {
let ty = &base.types[type_id as usize];
let params: Vec<&str> = ty.params.iter().copied().map(type_to_str).collect();
write!(
file,
"import \"{}.{}\" fn {}({})",
module,
name,
name,
params.join(", ")
)?;
if let Some(result) = ty.result {
write!(file, " -> {}", type_to_str(result))?;
}
writeln!(file, ";")?;
}
}
writeln!(file)?;
for &(name, value) in CONSTANTS {
writeln!(file, "const {} = 0x{:x};", name, value)?;
}
Ok(())
}
inner(File::create(path)?, self)
}
pub fn write_as_wat<P: AsRef<Path>>(&self, path: P) -> Result<()> {
fn inner(mut file: File, base: &BaseModule) -> Result<()> {
writeln!(file, ";; MicroW8 APIs, in WAT (Wasm Text) format")?;
writeln!(file, "(import \"env\" \"memory\" (memory {}))", base.memory)?;
writeln!(file)?;
for &(module, ref name, type_id) in &base.function_imports {
if !name.contains("reserved") {
let ty = &base.types[type_id as usize];
write!(file, "(import \"{}\" \"{}\" (func ${}", module, name, name)?;
for &param in &ty.params {
write!(file, " (param {})", type_to_str(param))?;
}
if let Some(result) = ty.result {
write!(file, " (result {})", type_to_str(result))?;
}
writeln!(file, "))")?;
}
}
writeln!(file)?;
writeln!(file, ";; to use defines, include this file with a preprocessor\n;; like gpp (https://logological.org/gpp).")?;
for &(name, value) in CONSTANTS {
writeln!(file, "#define {} 0x{:x};", name, value)?;
}
Ok(())
}
inner(File::create(path)?, self)
}
} }
fn add_function( fn add_function(
@@ -241,3 +399,30 @@ fn lookup_type(
}; };
*type_map.get(&key).unwrap() *type_map.get(&key).unwrap()
} }
fn type_to_str(ty: ValType) -> &'static str {
match ty {
ValType::I32 => "i32",
ValType::I64 => "i64",
ValType::F32 => "f32",
ValType::F64 => "f64",
_ => unimplemented!(),
}
}
const CONSTANTS: &[(&str, u32)] = &[
("TIME_MS", 0x40),
("GAMEPAD", 0x44),
("FRAMEBUFFER", 0x78),
("PALETTE", 0x13000),
("FONT", 0x13400),
("USER_MEM", 0x14000),
("BUTTON_UP", 0),
("BUTTON_DOWN", 1),
("BUTTON_LEFT", 2),
("BUTTON_RIGHT", 3),
("BUTTON_A", 4),
("BUTTON_B", 5),
("BUTTON_X", 6),
("BUTTON_Y", 7),
];

View File

@@ -1,13 +1,17 @@
use std::path::Path;
use anyhow::Result; use anyhow::Result;
use std::path::Path;
pub fn filter_exports(in_path: &Path, out_path: &Path) -> Result<()> { pub fn filter_exports(in_path: &Path, out_path: &Path) -> Result<()> {
let mut module = walrus::Module::from_file(in_path)?; let mut module = walrus::Module::from_file(in_path)?;
let exports_to_delete: Vec<_> = module.exports.iter().filter_map(|export| match export.name.as_str() { let exports_to_delete: Vec<_> = module
"upd" => None, .exports
_ => Some(export.id()) .iter()
}).collect(); .filter_map(|export| match export.name.as_str() {
"start" | "upd" | "snd" => None,
_ => Some(export.id()),
})
.collect();
for id in exports_to_delete { for id in exports_to_delete {
module.exports.delete(id); module.exports.delete(id);

View File

@@ -1,7 +1,15 @@
mod base_module; mod base_module;
mod pack;
mod filter_exports; mod filter_exports;
mod pack;
pub use base_module::BaseModule; pub use base_module::BaseModule;
pub use pack::{pack, pack_file, unpack, unpack_file, PackConfig};
pub use filter_exports::filter_exports; pub use filter_exports::filter_exports;
pub use pack::{pack, pack_file, unpack, unpack_file, PackConfig};
pub fn compressed_size(cart: &[u8]) -> f32 {
if cart[0] != 2 {
cart.len() as f32
} else {
upkr::compressed_size(&cart[1..]) + 1.
}
}

View File

@@ -1,8 +1,8 @@
use std::path::PathBuf; use std::path::PathBuf;
use anyhow::Result; use anyhow::Result;
use uw8_tool::BaseModule;
use pico_args::Arguments; use pico_args::Arguments;
use uw8_tool::BaseModule;
fn main() -> Result<()> { fn main() -> Result<()> {
let mut args = Arguments::from_env(); let mut args = Arguments::from_env();
@@ -32,6 +32,14 @@ fn main() -> Result<()> {
let dest: PathBuf = args.free_from_str()?; let dest: PathBuf = args.free_from_str()?;
uw8_tool::filter_exports(&source, &dest)?; uw8_tool::filter_exports(&source, &dest)?;
} }
"base-cwa" => {
let path: PathBuf = args.free_from_str()?;
BaseModule::for_format_version(1)?.write_as_cwa(path)?;
}
"base-wat" => {
let path: PathBuf = args.free_from_str()?;
BaseModule::for_format_version(1)?.write_as_wat(path)?;
}
_ => { _ => {
eprintln!("Unknown subcommand '{}'", cmd); eprintln!("Unknown subcommand '{}'", cmd);
print_help(); print_help();

View File

@@ -10,7 +10,7 @@ use std::{
use wasm_encoder as enc; use wasm_encoder as enc;
use wasmparser::{ use wasmparser::{
BinaryReader, ExportSectionReader, ExternalKind, FunctionBody, FunctionSectionReader, BinaryReader, ExportSectionReader, ExternalKind, FunctionBody, FunctionSectionReader,
ImportSectionEntryType, ImportSectionReader, TableSectionReader, TypeSectionReader, ImportSectionReader, TableSectionReader, TypeRef, TypeSectionReader,
}; };
pub struct PackConfig { pub struct PackConfig {
@@ -63,6 +63,7 @@ pub fn pack(data: &[u8], config: &PackConfig) -> Result<Vec<u8>> {
uw8.extend_from_slice(&upkr::pack( uw8.extend_from_slice(&upkr::pack(
&result[8..], &result[8..],
level, level,
&upkr::Config::default(),
Some(&mut |pos| { Some(&mut |pos| {
pb.set(pos as u64); pb.set(pos as u64);
}), }),
@@ -89,7 +90,10 @@ pub fn unpack(data: Vec<u8>) -> Result<Vec<u8>> {
let (version, data) = match data[0] { let (version, data) = match data[0] {
0 => return Ok(data), 0 => return Ok(data),
1 => (1, data[1..].to_vec()), 1 => (1, data[1..].to_vec()),
2 => (1, upkr::unpack(&data[1..])), 2 => (
1,
upkr::unpack(&data[1..], &upkr::Config::default(), 4 * 1024 * 1024)?,
),
other => bail!("Uknown format version {}", other), other => bail!("Uknown format version {}", other),
}; };
@@ -132,8 +136,8 @@ pub fn unpack(data: Vec<u8>) -> Result<Vec<u8>> {
Ok(dest) Ok(dest)
} }
fn to_val_type(type_: &wasmparser::Type) -> Result<ValType> { fn to_val_type(type_: &wasmparser::ValType) -> Result<ValType> {
use wasmparser::Type::*; use wasmparser::ValType::*;
Ok(match *type_ { Ok(match *type_ {
I32 => ValType::I32, I32 => ValType::I32,
I64 => ValType::I64, I64 => ValType::I64,
@@ -143,7 +147,7 @@ fn to_val_type(type_: &wasmparser::Type) -> Result<ValType> {
}) })
} }
fn to_val_type_vec(types: &[wasmparser::Type]) -> Result<Vec<ValType>> { fn to_val_type_vec(types: &[wasmparser::ValType]) -> Result<Vec<ValType>> {
types.into_iter().map(to_val_type).collect() types.into_iter().map(to_val_type).collect()
} }
@@ -201,7 +205,7 @@ impl<'a> ParsedModule<'a> {
import_section = Some(Section::new(range, ImportSection::parse(reader)?)); import_section = Some(Section::new(range, ImportSection::parse(reader)?));
} }
Payload::GlobalSection(reader) => { Payload::GlobalSection(reader) => {
global_section = Some(Section::new(range, reader.get_count())); global_section = Some(Section::new(range, reader.count()));
} }
Payload::FunctionSection(reader) => { Payload::FunctionSection(reader) => {
function_section = Some(Section::new(range, read_function_section(reader)?)); function_section = Some(Section::new(range, read_function_section(reader)?));
@@ -219,17 +223,22 @@ impl<'a> ParsedModule<'a> {
validate_table_section(reader)?; validate_table_section(reader)?;
table_section = Some(Section::new(range, ())); table_section = Some(Section::new(range, ()));
} }
Payload::ElementSection(mut reader) => { Payload::MemorySection(reader) => {
let mut elements = Vec::with_capacity(reader.get_count() as usize); if reader.count() != 0 {
for _ in 0..reader.get_count() { bail!("Found non-empty MemorySection. Memory has to be imported!");
elements.push(Element::parse(reader.read()?)?); }
}
Payload::ElementSection(reader) => {
let mut elements = Vec::with_capacity(reader.count() as usize);
for element in reader {
elements.push(Element::parse(element?)?);
} }
element_section = Some(elements); element_section = Some(elements);
} }
Payload::CodeSectionStart { .. } => (), Payload::CodeSectionStart { .. } => (),
Payload::CodeSectionEntry(body) => function_bodies.push(body), Payload::CodeSectionEntry(body) => function_bodies.push(body),
Payload::CustomSection { .. } => (), Payload::CustomSection { .. } => (),
Payload::End => break, Payload::End(..) => break,
other => bail!("Unsupported section: {:?}", other), other => bail!("Unsupported section: {:?}", other),
} }
@@ -457,7 +466,7 @@ impl<'a> ParsedModule<'a> {
{ {
let mut export_section = enc::ExportSection::new(); let mut export_section = enc::ExportSection::new();
for (name, fnc) in my_exports { for (name, fnc) in my_exports {
export_section.export(&name, enc::Export::Function(fnc)); export_section.export(&name, enc::ExportKind::Func, fnc);
} }
module.section(&export_section); module.section(&export_section);
} }
@@ -480,7 +489,7 @@ impl<'a> ParsedModule<'a> {
} }
element_section.active( element_section.active(
None, None,
&wasm_encoder::Instruction::I32Const(element.start_index as i32), &wasm_encoder::ConstExpr::i32_const(element.start_index as i32),
ValType::FuncRef, ValType::FuncRef,
wasm_encoder::Elements::Functions(&functions), wasm_encoder::Elements::Functions(&functions),
); );
@@ -529,28 +538,27 @@ fn read_type_section(reader: TypeSectionReader) -> Result<Vec<base_module::Funct
for type_def in reader { for type_def in reader {
match type_def? { match type_def? {
wasmparser::TypeDef::Func(fnc) => { wasmparser::Type::Func(fnc) => {
if fnc.returns.len() > 1 { if fnc.results().len() > 1 {
bail!("Multi-value not supported"); bail!("Multi-value not supported");
} }
let params = to_val_type_vec(&fnc.params)?; let params = to_val_type_vec(fnc.params())?;
let result = to_val_type_vec(&fnc.returns)?.into_iter().next(); let result = to_val_type_vec(fnc.results())?.into_iter().next();
function_types.push(FunctionType { params, result }); function_types.push(FunctionType { params, result });
} }
t => bail!("Unsupported type def {:?}", t),
} }
} }
Ok(function_types) Ok(function_types)
} }
fn validate_table_section(mut reader: TableSectionReader) -> Result<()> { fn validate_table_section(reader: TableSectionReader) -> Result<()> {
if reader.get_count() != 1 { if reader.count() != 1 {
bail!("Only up to one table supported"); bail!("Only up to one table supported");
} }
let type_ = reader.read()?; let type_ = reader.into_iter().next().unwrap()?;
if type_.element_type != wasmparser::Type::FuncRef { if type_.element_type != wasmparser::ValType::FuncRef {
bail!("Only one funcref table is supported"); bail!("Only one funcref table is supported");
} }
@@ -584,21 +592,20 @@ impl ImportSection {
for import in reader { for import in reader {
let import = import?; let import = import?;
if let Some(field) = import.field {
match import.ty { match import.ty {
ImportSectionEntryType::Function(type_) => { TypeRef::Func(type_) => {
functions.push(FunctionImport { functions.push(FunctionImport {
module: import.module.to_string(), module: import.module.to_string(),
field: field.to_string(), field: import.name.to_string(),
type_, type_,
}); });
} }
ImportSectionEntryType::Memory(mem) => { TypeRef::Memory(mem) => {
if import.module != "env" || field != "memory" { if import.module != "env" || import.name != "memory" {
bail!( bail!(
"Wrong name of memory import {}.{}, should be env.memory", "Wrong name of memory import {}.{}, should be env.memory",
import.module, import.module,
field import.name
); );
} }
if mem.memory64 || mem.shared { if mem.memory64 || mem.shared {
@@ -606,10 +613,10 @@ impl ImportSection {
} }
memory = mem.maximum.unwrap_or(mem.initial) as u32; memory = mem.maximum.unwrap_or(mem.initial) as u32;
} }
ImportSectionEntryType::Global(glbl) => { TypeRef::Global(glbl) => {
globals.push(GlobalImport { globals.push(GlobalImport {
module: import.module.to_string(), module: import.module.to_string(),
field: field.to_string(), field: import.name.to_string(),
type_: GlobalType { type_: GlobalType {
type_: to_val_type(&glbl.content_type)?, type_: to_val_type(&glbl.content_type)?,
mutable: glbl.mutable, mutable: glbl.mutable,
@@ -618,12 +625,6 @@ impl ImportSection {
} }
_ => bail!("Unsupported import item {:?}", import.ty), _ => bail!("Unsupported import item {:?}", import.ty),
} }
} else {
bail!(
"Found import without field, only module '{}'",
import.module
);
}
} }
Ok(ImportSection { Ok(ImportSection {
@@ -642,17 +643,17 @@ struct Element {
impl Element { impl Element {
fn parse(element: wasmparser::Element) -> Result<Element> { fn parse(element: wasmparser::Element) -> Result<Element> {
if element.ty != wasmparser::Type::FuncRef { match element.items {
bail!("Table element type is not FuncRef"); wasmparser::ElementItems::Functions(funcs_reader) => {
}
let start_index = if let wasmparser::ElementKind::Active { let start_index = if let wasmparser::ElementKind::Active {
init_expr, offset_expr,
table_index: 0, table_index: 0,
} = element.kind } = element.kind
{ {
let mut init_reader = init_expr.get_operators_reader(); let mut init_reader = offset_expr.get_operators_reader();
if let wasmparser::Operator::I32Const { value: start_index } = init_reader.read()? { if let wasmparser::Operator::I32Const { value: start_index } =
init_reader.read()?
{
start_index as u32 start_index as u32
} else { } else {
bail!("Table element start index is not a integer constant"); bail!("Table element start index is not a integer constant");
@@ -661,15 +662,9 @@ impl Element {
bail!("Unsupported table element kind"); bail!("Unsupported table element kind");
}; };
let mut items_reader = element.items.get_items_reader()?; let mut functions = Vec::with_capacity(funcs_reader.count() as usize);
for index in funcs_reader {
let mut functions = Vec::with_capacity(items_reader.get_count() as usize); functions.push(index?);
for _ in 0..items_reader.get_count() {
if let wasmparser::ElementItem::Func(index) = items_reader.read()? {
functions.push(index);
} else {
bail!("Table element item is not a function");
}
} }
Ok(Element { Ok(Element {
@@ -677,6 +672,9 @@ impl Element {
functions, functions,
}) })
} }
_ => bail!("Table element type is not FuncRef"),
}
}
} }
#[derive(Debug)] #[derive(Debug)]
@@ -706,8 +704,8 @@ fn read_export_section(reader: ExportSectionReader) -> Result<Vec<(String, u32)>
for export in reader { for export in reader {
let export = export?; let export = export?;
match export.kind { match export.kind {
ExternalKind::Function => { ExternalKind::Func => {
function_exports.push((export.field.to_string(), export.index)); function_exports.push((export.name.to_string(), export.index));
} }
_ => (), // just ignore all other kinds since MicroW8 doesn't expect any exports other than functions _ => (), // just ignore all other kinds since MicroW8 doesn't expect any exports other than functions
} }
@@ -728,13 +726,11 @@ fn remap_function(
} }
let mut function = enc::Function::new(locals); let mut function = enc::Function::new(locals);
let block_type = |ty: wasmparser::TypeOrFuncType| -> Result<enc::BlockType> { let block_type = |ty: wasmparser::BlockType| -> Result<enc::BlockType> {
Ok(match ty { Ok(match ty {
wasmparser::TypeOrFuncType::Type(wasmparser::Type::EmptyBlockType) => { wasmparser::BlockType::Empty => enc::BlockType::Empty,
enc::BlockType::Empty wasmparser::BlockType::Type(ty) => enc::BlockType::Result(to_val_type(&ty)?),
} wasmparser::BlockType::FuncType(ty) => enc::BlockType::FunctionType(
wasmparser::TypeOrFuncType::Type(ty) => enc::BlockType::Result(to_val_type(&ty)?),
wasmparser::TypeOrFuncType::FuncType(ty) => enc::BlockType::FunctionType(
*type_map *type_map
.get(&ty) .get(&ty)
.ok_or_else(|| anyhow!("Function type index out of range: {}", ty))?, .ok_or_else(|| anyhow!("Function type index out of range: {}", ty))?,
@@ -748,7 +744,7 @@ fn remap_function(
.ok_or_else(|| anyhow!("Global index out of range: {}", idx))?) .ok_or_else(|| anyhow!("Global index out of range: {}", idx))?)
}; };
fn mem(m: wasmparser::MemoryImmediate) -> enc::MemArg { fn mem(m: wasmparser::MemArg) -> enc::MemArg {
enc::MemArg { enc::MemArg {
offset: m.offset, offset: m.offset,
align: m.align as u32, align: m.align as u32,
@@ -763,24 +759,31 @@ fn remap_function(
function.instruction(&match op? { function.instruction(&match op? {
De::Unreachable => En::Unreachable, De::Unreachable => En::Unreachable,
De::Nop => En::Nop, De::Nop => En::Nop,
De::Block { ty } => En::Block(block_type(ty)?), De::Block { blockty } => En::Block(block_type(blockty)?),
De::Loop { ty } => En::Loop(block_type(ty)?), De::Loop { blockty } => En::Loop(block_type(blockty)?),
De::If { ty } => En::If(block_type(ty)?), De::If { blockty } => En::If(block_type(blockty)?),
De::Else => En::Else, De::Else => En::Else,
De::Try { .. } | De::Catch { .. } | De::Throw { .. } | De::Rethrow { .. } => todo!(), De::Try { .. } | De::Catch { .. } | De::Throw { .. } | De::Rethrow { .. } => todo!(),
De::End => En::End, De::End => En::End,
De::Br { relative_depth } => En::Br(relative_depth), De::Br { relative_depth } => En::Br(relative_depth),
De::BrIf { relative_depth } => En::BrIf(relative_depth), De::BrIf { relative_depth } => En::BrIf(relative_depth),
De::BrTable { .. } => todo!(), De::BrTable { targets } => En::BrTable(
targets.targets().collect::<Result<Vec<u32>, _>>()?.into(),
targets.default(),
),
De::Return => En::Return, De::Return => En::Return,
De::Call { function_index } => En::Call( De::Call { function_index } => En::Call(
*function_map *function_map
.get(&function_index) .get(&function_index)
.ok_or_else(|| anyhow!("Function index out of range: {}", function_index))?, .ok_or_else(|| anyhow!("Function index out of range: {}", function_index))?,
), ),
De::CallIndirect { index, table_index } => En::CallIndirect { De::CallIndirect {
type_index,
table_index,
table_byte: _, // what is this supposed to be?
} => En::CallIndirect {
ty: *type_map ty: *type_map
.get(&index) .get(&type_index)
.ok_or_else(|| anyhow!("Unknown function type in call indirect"))?, .ok_or_else(|| anyhow!("Unknown function type in call indirect"))?,
table: table_index, table: table_index,
}, },
@@ -800,16 +803,16 @@ fn remap_function(
De::I64Load { memarg } => En::I64Load(mem(memarg)), De::I64Load { memarg } => En::I64Load(mem(memarg)),
De::F32Load { memarg } => En::F32Load(mem(memarg)), De::F32Load { memarg } => En::F32Load(mem(memarg)),
De::F64Load { memarg } => En::F64Load(mem(memarg)), De::F64Load { memarg } => En::F64Load(mem(memarg)),
De::I32Load8S { memarg } => En::I32Load8_S(mem(memarg)), De::I32Load8S { memarg } => En::I32Load8S(mem(memarg)),
De::I32Load8U { memarg } => En::I32Load8_U(mem(memarg)), De::I32Load8U { memarg } => En::I32Load8U(mem(memarg)),
De::I32Load16S { memarg } => En::I32Load16_S(mem(memarg)), De::I32Load16S { memarg } => En::I32Load16S(mem(memarg)),
De::I32Load16U { memarg } => En::I32Load16_U(mem(memarg)), De::I32Load16U { memarg } => En::I32Load16U(mem(memarg)),
De::I64Load8S { memarg } => En::I64Load8_S(mem(memarg)), De::I64Load8S { memarg } => En::I64Load8S(mem(memarg)),
De::I64Load8U { memarg } => En::I64Load8_U(mem(memarg)), De::I64Load8U { memarg } => En::I64Load8U(mem(memarg)),
De::I64Load16S { memarg } => En::I64Load16_S(mem(memarg)), De::I64Load16S { memarg } => En::I64Load16S(mem(memarg)),
De::I64Load16U { memarg } => En::I64Load16_U(mem(memarg)), De::I64Load16U { memarg } => En::I64Load16U(mem(memarg)),
De::I64Load32S { memarg } => En::I64Load32_S(mem(memarg)), De::I64Load32S { memarg } => En::I64Load32S(mem(memarg)),
De::I64Load32U { memarg } => En::I64Load32_U(mem(memarg)), De::I64Load32U { memarg } => En::I64Load32U(mem(memarg)),
De::I32Store { memarg } => En::I32Store(mem(memarg)), De::I32Store { memarg } => En::I32Store(mem(memarg)),
De::I64Store { memarg } => En::I64Store(mem(memarg)), De::I64Store { memarg } => En::I64Store(mem(memarg)),
De::F32Store { memarg } => En::F32Store(mem(memarg)), De::F32Store { memarg } => En::F32Store(mem(memarg)),
@@ -828,7 +831,7 @@ fn remap_function(
De::RefNull { .. } | De::RefIsNull { .. } | De::RefFunc { .. } => todo!(), De::RefNull { .. } | De::RefIsNull { .. } | De::RefFunc { .. } => todo!(),
De::I32Eqz => En::I32Eqz, De::I32Eqz => En::I32Eqz,
De::I32Eq => En::I32Eq, De::I32Eq => En::I32Eq,
De::I32Ne => En::I32Neq, De::I32Ne => En::I32Ne,
De::I32LtS => En::I32LtS, De::I32LtS => En::I32LtS,
De::I32LtU => En::I32LtU, De::I32LtU => En::I32LtU,
De::I32GtS => En::I32GtS, De::I32GtS => En::I32GtS,
@@ -839,7 +842,7 @@ fn remap_function(
De::I32GeU => En::I32GeU, De::I32GeU => En::I32GeU,
De::I64Eqz => En::I64Eqz, De::I64Eqz => En::I64Eqz,
De::I64Eq => En::I64Eq, De::I64Eq => En::I64Eq,
De::I64Ne => En::I64Neq, De::I64Ne => En::I64Ne,
De::I64LtS => En::I64LtS, De::I64LtS => En::I64LtS,
De::I64LtU => En::I64LtU, De::I64LtU => En::I64LtU,
De::I64GtS => En::I64GtS, De::I64GtS => En::I64GtS,
@@ -849,13 +852,13 @@ fn remap_function(
De::I64GeS => En::I64GeS, De::I64GeS => En::I64GeS,
De::I64GeU => En::I64GeU, De::I64GeU => En::I64GeU,
De::F32Eq => En::F32Eq, De::F32Eq => En::F32Eq,
De::F32Ne => En::F32Neq, De::F32Ne => En::F32Ne,
De::F32Lt => En::F32Lt, De::F32Lt => En::F32Lt,
De::F32Gt => En::F32Gt, De::F32Gt => En::F32Gt,
De::F32Le => En::F32Le, De::F32Le => En::F32Le,
De::F32Ge => En::F32Ge, De::F32Ge => En::F32Ge,
De::F64Eq => En::F64Eq, De::F64Eq => En::F64Eq,
De::F64Ne => En::F64Neq, De::F64Ne => En::F64Ne,
De::F64Lt => En::F64Lt, De::F64Lt => En::F64Lt,
De::F64Gt => En::F64Gt, De::F64Gt => En::F64Gt,
De::F64Le => En::F64Le, De::F64Le => En::F64Le,
@@ -962,6 +965,8 @@ fn remap_function(
De::I64TruncSatF32U => En::I64TruncSatF32U, De::I64TruncSatF32U => En::I64TruncSatF32U,
De::I64TruncSatF64S => En::I64TruncSatF64S, De::I64TruncSatF64S => En::I64TruncSatF64S,
De::I64TruncSatF64U => En::I64TruncSatF64U, De::I64TruncSatF64U => En::I64TruncSatF64U,
De::MemoryCopy { src_mem, dst_mem } => En::MemoryCopy { src_mem, dst_mem },
De::MemoryFill { mem } => En::MemoryFill(mem),
other => bail!("Unsupported instruction {:?}", other), other => bail!("Unsupported instruction {:?}", other),
}); });
} }

1
uw8-window/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

2417
uw8-window/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

18
uw8-window/Cargo.toml Normal file
View File

@@ -0,0 +1,18 @@
[package]
name = "uw8-window"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
winit = "0.29.15"
env_logger = "0.11.3"
log = "0.4"
pico-args = "0.5"
wgpu = "0.19.3"
pollster = "0.3.0"
bytemuck = { version = "1.15", features = [ "derive" ] }
anyhow = "1"
minifb = { version = "0.25.0", default-features = false, features = ["x11"] }
winapi = { version = "0.3.9", features = [ "timeapi" ] }

83
uw8-window/src/cpu.rs Normal file
View File

@@ -0,0 +1,83 @@
use std::time::Instant;
use crate::{Input, WindowImpl};
use anyhow::Result;
use minifb::{Key, WindowOptions};
static GAMEPAD_KEYS: &[Key] = &[
Key::Up,
Key::Down,
Key::Left,
Key::Right,
Key::Z,
Key::X,
Key::A,
Key::S,
];
pub struct Window {
window: minifb::Window,
buffer: Vec<u32>,
}
impl Window {
pub fn new() -> Result<Window> {
#[cfg(target_os = "windows")]
unsafe {
winapi::um::timeapi::timeBeginPeriod(1);
}
let buffer: Vec<u32> = vec![0; 320 * 240];
let options = WindowOptions {
scale: minifb::Scale::X2,
scale_mode: minifb::ScaleMode::AspectRatioStretch,
resize: true,
..Default::default()
};
let window = minifb::Window::new("MicroW8", 320, 240, options).unwrap();
Ok(Window { window, buffer })
}
}
impl WindowImpl for Window {
fn begin_frame(&mut self) -> Input {
let mut gamepads = [0u8; 4];
for key in self.window.get_keys() {
if let Some(index) = GAMEPAD_KEYS
.iter()
.enumerate()
.find(|(_, &k)| k == key)
.map(|(i, _)| i)
{
gamepads[0] |= 1 << index;
}
}
Input {
gamepads,
reset: self.window.is_key_pressed(Key::R, minifb::KeyRepeat::No),
}
}
fn end_frame(&mut self, framebuffer: &[u8], palette: &[u8], next_frame: Instant) {
for (i, &color_index) in framebuffer.iter().enumerate() {
let offset = color_index as usize * 4;
self.buffer[i] = 0xff000000
| ((palette[offset] as u32) << 16)
| ((palette[offset + 1] as u32) << 8)
| palette[offset + 2] as u32;
}
self.window
.update_with_buffer(&self.buffer, 320, 240)
.unwrap();
if let Some(sleep) = next_frame.checked_duration_since(Instant::now()) {
std::thread::sleep(sleep);
}
}
fn is_open(&self) -> bool {
self.window.is_open() && !self.window.is_key_down(Key::Escape)
}
}

143
uw8-window/src/gpu/crt.rs Normal file
View File

@@ -0,0 +1,143 @@
use wgpu::util::DeviceExt;
use winit::dpi::PhysicalSize;
use super::Filter;
pub struct CrtFilter {
uniform_buffer: wgpu::Buffer,
bind_group: wgpu::BindGroup,
pipeline: wgpu::RenderPipeline,
}
impl CrtFilter {
pub fn new(
device: &wgpu::Device,
screen: &wgpu::TextureView,
resolution: PhysicalSize<u32>,
surface_format: wgpu::TextureFormat,
) -> CrtFilter {
let uniforms = Uniforms {
texture_scale: texture_scale_from_resolution(resolution),
};
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: None,
contents: bytemuck::cast_slice(&[uniforms]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let crt_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: false },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
label: None,
});
let crt_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &crt_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&screen),
},
wgpu::BindGroupEntry {
binding: 1,
resource: uniform_buffer.as_entire_binding(),
},
],
label: None,
});
let crt_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(include_str!("crt.wgsl").into()),
});
let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[&crt_bind_group_layout],
push_constant_ranges: &[],
});
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: &crt_shader,
entry_point: "vs_main",
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &crt_shader,
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format: surface_format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: Default::default(),
depth_stencil: None,
multisample: Default::default(),
multiview: None,
});
CrtFilter {
uniform_buffer,
bind_group: crt_bind_group,
pipeline: render_pipeline,
}
}
}
impl Filter for CrtFilter {
fn resize(&mut self, queue: &wgpu::Queue, new_size: PhysicalSize<u32>) {
let uniforms = Uniforms {
texture_scale: texture_scale_from_resolution(new_size),
};
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
}
fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {
render_pass.set_pipeline(&self.pipeline);
render_pass.set_bind_group(0, &self.bind_group, &[]);
render_pass.draw(0..6, 0..1);
}
}
fn texture_scale_from_resolution(res: PhysicalSize<u32>) -> [f32; 4] {
let scale = ((res.width as f32) / 160.0).min((res.height as f32) / 120.0);
[
res.width as f32 / scale,
res.height as f32 / scale,
2.0 / scale,
0.0,
]
}
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct Uniforms {
texture_scale: [f32; 4],
}

View File

@@ -0,0 +1,75 @@
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) tex_coords: vec2<f32>,
}
struct Uniforms {
texture_scale: vec4<f32>,
}
@group(0) @binding(1) var<uniform> uniforms: Uniforms;
@vertex
fn vs_main(
@builtin(vertex_index) in_vertex_index: u32,
) -> VertexOutput {
var out: VertexOutput;
let i = in_vertex_index / 3u + in_vertex_index % 3u;
let x = -1.0 + f32(i % 2u) * 322.0;
let y = -1.0 + f32(i / 2u) * 242.0;
out.clip_position = vec4<f32>((vec2<f32>(x, y) - vec2<f32>(160.0, 120.0)) / uniforms.texture_scale.xy, 0.0, 1.0);
out.tex_coords = vec2<f32>(x, y);
return out;
}
@group(0) @binding(0) var screen_texture: texture_2d<f32>;
fn sample_pixel(coords: vec2<i32>, offset: vec4<f32>) -> vec3<f32> {
let is_outside = any(vec2<u32>(coords) >= vec2<u32>(320u, 240u));
if(is_outside) {
return vec3<f32>(0.0);
} else {
let f = max(vec4<f32>(0.008) / offset - vec4<f32>(0.0024), vec4<f32>(0.0));
return textureLoad(screen_texture, coords, 0).rgb * (f.x + f.y + f.z + f.w);
}
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let pixelf = floor(in.tex_coords);
let o = vec2<f32>(0.5) - (in.tex_coords - pixelf);
let pixel = vec2<i32>(pixelf);
let offset_x = o.xxxx + vec4<f32>(-0.125, 0.375, 0.125, -0.375) * uniforms.texture_scale.z;
let offset_y = o.yyyy + vec4<f32>(-0.375, -0.125, 0.375, 0.125) * uniforms.texture_scale.z;
var offset_x0 = max(abs(offset_x + vec4<f32>(-1.0)) - vec4<f32>(0.5), vec4<f32>(0.0));
var offset_x1 = max(abs(offset_x) - vec4<f32>(0.5), vec4<f32>(0.0));
var offset_x2 = max(abs(offset_x + vec4<f32>(1.0)) - vec4<f32>(0.5), vec4<f32>(0.0));
offset_x0 = offset_x0 * offset_x0;
offset_x1 = offset_x1 * offset_x1;
offset_x2 = offset_x2 * offset_x2;
var offset_yr = offset_y + vec4<f32>(-1.0);
offset_yr = vec4<f32>(0.02) + offset_yr * offset_yr;
var acc = sample_pixel(pixel + vec2<i32>(-1, -1), offset_x0 + offset_yr);
acc = acc + sample_pixel(pixel + vec2<i32>(0, -1), offset_x1 + offset_yr);
acc = acc + sample_pixel(pixel + vec2<i32>(1, -1), offset_x2 + offset_yr);
offset_yr = vec4<f32>(0.02) + offset_y * offset_y;
acc = acc + sample_pixel(pixel + vec2<i32>(-1, 0), offset_x0 + offset_yr);
acc = acc + sample_pixel(pixel, offset_x1 + offset_yr);
acc = acc + sample_pixel(pixel + vec2<i32>(1, 0), offset_x2 + offset_yr);
offset_yr = offset_y + vec4<f32>(1.0);
offset_yr = vec4<f32>(0.02) + offset_yr * offset_yr;
acc = acc + sample_pixel(pixel + vec2<i32>(-1, 1), offset_x0 + offset_yr);
acc = acc + sample_pixel(pixel + vec2<i32>(0, 1), offset_x1 + offset_yr);
acc = acc + sample_pixel(pixel + vec2<i32>(1, 1), offset_x2 + offset_yr);
return vec4<f32>(acc, 1.0);
}

View File

@@ -0,0 +1,162 @@
use wgpu::util::DeviceExt;
use winit::dpi::PhysicalSize;
use super::Filter;
pub struct FastCrtFilter {
uniform_buffer: wgpu::Buffer,
bind_group: wgpu::BindGroup,
pipeline: wgpu::RenderPipeline,
}
impl FastCrtFilter {
pub fn new(
device: &wgpu::Device,
screen: &wgpu::TextureView,
resolution: PhysicalSize<u32>,
surface_format: wgpu::TextureFormat,
chromatic: bool,
) -> FastCrtFilter {
let uniforms = Uniforms {
texture_scale: texture_scale_from_resolution(resolution),
};
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: None,
contents: bytemuck::cast_slice(&[uniforms]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
label: None,
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
mag_filter: wgpu::FilterMode::Linear,
..Default::default()
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&screen),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: uniform_buffer.as_entire_binding(),
},
],
label: None,
});
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(include_str!("fast_crt.wgsl").into()),
});
let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: if chromatic {
"fs_main_chromatic"
} else {
"fs_main"
},
targets: &[Some(wgpu::ColorTargetState {
format: surface_format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: Default::default(),
depth_stencil: None,
multisample: Default::default(),
multiview: None,
});
FastCrtFilter {
uniform_buffer,
bind_group,
pipeline: render_pipeline,
}
}
}
impl Filter for FastCrtFilter {
fn resize(&mut self, queue: &wgpu::Queue, new_size: PhysicalSize<u32>) {
let uniforms = Uniforms {
texture_scale: texture_scale_from_resolution(new_size),
};
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
}
fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {
render_pass.set_pipeline(&self.pipeline);
render_pass.set_bind_group(0, &self.bind_group, &[]);
render_pass.draw(0..6, 0..1);
}
}
fn texture_scale_from_resolution(res: PhysicalSize<u32>) -> [f32; 4] {
let scale = ((res.width as f32) / 160.0).min((res.height as f32) / 120.0);
[
scale / res.width as f32,
scale / res.height as f32,
2.0 / scale,
0.0,
]
}
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct Uniforms {
texture_scale: [f32; 4],
}

View File

@@ -0,0 +1,66 @@
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) tex_coords: vec2<f32>,
}
struct Uniforms {
texture_scale: vec4<f32>,
}
@group(0) @binding(2) var<uniform> uniforms: Uniforms;
@vertex
fn vs_main(
@builtin(vertex_index) in_vertex_index: u32,
) -> VertexOutput {
var out: VertexOutput;
let i = in_vertex_index / 3u + in_vertex_index % 3u;
let x = 0.0 + f32(i % 2u) * 320.0;
let y = 0.0 + f32(i / 2u) * 240.0;
out.clip_position = vec4<f32>((vec2<f32>(x, y) - vec2<f32>(160.0, 120.0)) * uniforms.texture_scale.xy, 0.0, 1.0);
out.tex_coords = vec2<f32>(x, y);
return out;
}
@group(0) @binding(0) var screen_texture: texture_2d<f32>;
@group(0) @binding(1) var linear_sampler: sampler;
fn row_factor(offset: f32) -> f32 {
return 1.0 / (1.0 + offset * offset * 16.0);
}
fn col_factor(offset: f32) -> f32 {
let o = max(0.0, abs(offset) - 0.4);
return 1.0 / (1.0 + o * o * 16.0);
}
fn sample_screen(tex_coords: vec2<f32>) -> vec4<f32> {
let base = round(tex_coords) - vec2<f32>(0.5);
let frac = tex_coords - base;
let top_factor = row_factor(frac.y);
let bottom_factor = row_factor(frac.y - 1.0);
let v = base.y + bottom_factor / (bottom_factor + top_factor);
let left_factor = col_factor(frac.x);
let right_factor = col_factor(frac.x - 1.0);
let u = base.x + right_factor / (right_factor + left_factor);
return textureSample(screen_texture, linear_sampler, vec2<f32>(u, v) / vec2<f32>(320.0, 240.0)) * (top_factor + bottom_factor) * (left_factor + right_factor) * 1.1;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return sample_screen(in.tex_coords);
}
@fragment
fn fs_main_chromatic(in: VertexOutput) -> @location(0) vec4<f32> {
let r = sample_screen(in.tex_coords + vec2<f32>(0.2, 0.2)).r;
let g = sample_screen(in.tex_coords + vec2<f32>(0.07, -0.27)).g;
let b = sample_screen(in.tex_coords + vec2<f32>(-0.27, 0.07)).b;
return vec4<f32>(r, g, b, 1.0);
}

565
uw8-window/src/gpu/mod.rs Normal file
View File

@@ -0,0 +1,565 @@
use crate::{Input, WindowConfig, WindowImpl};
use anyhow::{anyhow, Result};
use std::{sync::Arc, time::Instant};
use winit::{
dpi::PhysicalSize,
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop},
keyboard::{Key, KeyCode, NamedKey, PhysicalKey},
platform::pump_events::{EventLoopExtPumpEvents, PumpStatus},
window::{Fullscreen, WindowBuilder},
};
mod crt;
mod fast_crt;
mod square;
use crt::CrtFilter;
use fast_crt::FastCrtFilter;
use square::SquareFilter;
pub struct Window {
_instance: wgpu::Instance,
surface: wgpu::Surface<'static>,
_adapter: wgpu::Adapter,
device: wgpu::Device,
queue: wgpu::Queue,
palette_screen_mode: PaletteScreenMode,
surface_config: wgpu::SurfaceConfiguration,
filter: Box<dyn Filter>,
event_loop: EventLoop<()>,
window: Arc<winit::window::Window>,
gamepads: [u8; 4],
next_frame: Instant,
is_fullscreen: bool,
is_open: bool,
}
impl Window {
pub fn new(window_config: WindowConfig) -> Result<Window> {
async fn create(window_config: WindowConfig) -> Result<Window> {
let event_loop = EventLoop::new()?;
let window = WindowBuilder::new()
.with_inner_size(PhysicalSize::new(
(320. * window_config.scale).round() as u32,
(240. * window_config.scale).round() as u32,
))
.with_min_inner_size(PhysicalSize::new(320u32, 240))
.with_title("MicroW8")
.with_fullscreen(if window_config.fullscreen {
Some(Fullscreen::Borderless(None))
} else {
None
})
.build(&event_loop)?;
window.set_cursor_visible(false);
let window = Arc::new(window);
let instance = wgpu::Instance::new(Default::default());
let surface = instance.create_surface(window.clone())?;
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::LowPower,
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.await
.ok_or_else(|| anyhow!("Request adapter failed"))?;
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor::default(), None)
.await?;
let palette_screen_mode = PaletteScreenMode::new(&device);
let surface_config = wgpu::SurfaceConfiguration {
present_mode: wgpu::PresentMode::AutoNoVsync,
..surface
.get_default_config(
&adapter,
window.inner_size().width,
window.inner_size().height,
)
.expect("Surface incompatible with adapter")
};
let filter: Box<dyn Filter> = create_filter(
&device,
&palette_screen_mode.screen_view,
window.inner_size(),
surface_config.format,
window_config.filter,
);
surface.configure(&device, &surface_config);
Ok(Window {
event_loop,
window,
_instance: instance,
surface,
_adapter: adapter,
device,
queue,
palette_screen_mode,
surface_config,
filter,
gamepads: [0; 4],
next_frame: Instant::now(),
is_fullscreen: window_config.fullscreen,
is_open: true,
})
}
pollster::block_on(create(window_config))
}
}
impl WindowImpl for Window {
fn begin_frame(&mut self) -> Input {
let mut reset = false;
self.event_loop
.set_control_flow(ControlFlow::WaitUntil(self.next_frame));
while self.is_open {
let timeout = self.next_frame.saturating_duration_since(Instant::now());
let status = self.event_loop.pump_events(Some(timeout), |event, elwt| {
let mut new_filter = None;
match event {
Event::WindowEvent { event, .. } => match event {
WindowEvent::Resized(new_size) => {
self.surface_config.width = new_size.width;
self.surface_config.height = new_size.height;
self.surface.configure(&self.device, &self.surface_config);
self.filter.resize(&self.queue, new_size);
}
WindowEvent::CloseRequested => {
elwt.exit();
}
WindowEvent::KeyboardInput { event, .. } => {
fn gamepad_button(input: &winit::event::KeyEvent) -> u8 {
match input.physical_key {
PhysicalKey::Code(KeyCode::KeyZ) => 16,
PhysicalKey::Code(KeyCode::KeyX) => 32,
PhysicalKey::Code(KeyCode::KeyA) => 64,
PhysicalKey::Code(KeyCode::KeyS) => 128,
_ => match input.logical_key {
Key::Named(NamedKey::ArrowUp) => 1,
Key::Named(NamedKey::ArrowDown) => 2,
Key::Named(NamedKey::ArrowLeft) => 4,
Key::Named(NamedKey::ArrowRight) => 8,
_ => 0,
},
}
}
if event.state == winit::event::ElementState::Pressed {
match event.logical_key {
Key::Named(NamedKey::Escape) => {
elwt.exit();
}
Key::Character(ref c) => match c.as_str() {
"f" => {
let fullscreen = if self.window.fullscreen().is_some() {
None
} else {
Some(Fullscreen::Borderless(None))
};
self.is_fullscreen = fullscreen.is_some();
self.window.set_fullscreen(fullscreen);
}
"r" => reset = true,
"1" => new_filter = Some(1),
"2" => new_filter = Some(2),
"3" => new_filter = Some(3),
"4" => new_filter = Some(4),
"5" => new_filter = Some(5),
_ => (),
},
_ => (),
}
self.gamepads[0] |= gamepad_button(&event);
} else {
self.gamepads[0] &= !gamepad_button(&event);
}
}
_ => (),
},
_ => (),
}
if let Some(new_filter) = new_filter {
self.filter = create_filter(
&self.device,
&self.palette_screen_mode.screen_view,
self.window.inner_size(),
self.surface_config.format,
new_filter,
);
}
});
match status {
PumpStatus::Exit(_) => self.is_open = false,
_ => (),
}
if Instant::now() >= self.next_frame {
break;
}
}
Input {
gamepads: self.gamepads,
reset,
}
}
fn end_frame(&mut self, framebuffer: &[u8], palette: &[u8], next_frame: Instant) {
self.next_frame = next_frame;
self.palette_screen_mode
.write_framebuffer(&self.queue, framebuffer);
self.palette_screen_mode.write_palette(&self.queue, palette);
let output = self.surface.get_current_texture().unwrap();
let view = output
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
self.palette_screen_mode.resolve_screen(&mut encoder);
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: None,
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
self.filter.render(&mut render_pass);
}
self.queue.submit(std::iter::once(encoder.finish()));
output.present();
}
fn is_open(&self) -> bool {
self.is_open
}
}
fn create_filter(
device: &wgpu::Device,
screen_texture: &wgpu::TextureView,
window_size: PhysicalSize<u32>,
surface_format: wgpu::TextureFormat,
filter: u32,
) -> Box<dyn Filter> {
match filter {
1 => Box::new(SquareFilter::new(
device,
screen_texture,
window_size,
surface_format,
)),
2 => Box::new(FastCrtFilter::new(
device,
screen_texture,
window_size,
surface_format,
false,
)),
3 => Box::new(CrtFilter::new(
device,
screen_texture,
window_size,
surface_format,
)),
4 => Box::new(FastCrtFilter::new(
device,
screen_texture,
window_size,
surface_format,
true,
)),
_ => Box::new(AutoCrtFilter::new(
device,
screen_texture,
window_size,
surface_format,
)),
}
}
trait Filter {
fn resize(&mut self, queue: &wgpu::Queue, new_size: PhysicalSize<u32>);
fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>);
}
struct AutoCrtFilter {
small: CrtFilter,
large: FastCrtFilter,
resolution: PhysicalSize<u32>,
}
impl AutoCrtFilter {
fn new(
device: &wgpu::Device,
screen: &wgpu::TextureView,
resolution: PhysicalSize<u32>,
surface_format: wgpu::TextureFormat,
) -> AutoCrtFilter {
let small = CrtFilter::new(device, screen, resolution, surface_format);
let large = FastCrtFilter::new(device, screen, resolution, surface_format, true);
AutoCrtFilter {
small,
large,
resolution,
}
}
}
impl Filter for AutoCrtFilter {
fn resize(&mut self, queue: &wgpu::Queue, new_size: PhysicalSize<u32>) {
self.small.resize(queue, new_size);
self.large.resize(queue, new_size);
self.resolution = new_size;
}
fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {
if self.resolution.width < 960 || self.resolution.height < 720 {
self.small.render(render_pass);
} else {
self.large.render(render_pass);
}
}
}
struct PaletteScreenMode {
framebuffer: wgpu::Texture,
palette: wgpu::Texture,
screen_view: wgpu::TextureView,
bind_group: wgpu::BindGroup,
pipeline: wgpu::RenderPipeline,
}
impl PaletteScreenMode {
fn new(device: &wgpu::Device) -> PaletteScreenMode {
let framebuffer_texture = device.create_texture(&wgpu::TextureDescriptor {
size: wgpu::Extent3d {
width: 320,
height: 240,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::R8Uint,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
label: None,
view_formats: &[],
});
let palette_texture = device.create_texture(&wgpu::TextureDescriptor {
size: wgpu::Extent3d {
width: 256,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D1,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
label: None,
view_formats: &[],
});
let screen_texture = device.create_texture(&wgpu::TextureDescriptor {
size: wgpu::Extent3d {
width: 320,
height: 240,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
label: None,
view_formats: &[],
});
let framebuffer_texture_view =
framebuffer_texture.create_view(&wgpu::TextureViewDescriptor::default());
let palette_texture_view =
palette_texture.create_view(&wgpu::TextureViewDescriptor::default());
let screen_texture_view =
screen_texture.create_view(&wgpu::TextureViewDescriptor::default());
let palette_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Uint,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D1,
sample_type: wgpu::TextureSampleType::Float { filterable: false },
},
count: None,
},
],
label: None,
});
let palette_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &palette_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&framebuffer_texture_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&palette_texture_view),
},
],
label: None,
});
let palette_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(include_str!("palette.wgsl").into()),
});
let palette_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[&palette_bind_group_layout],
push_constant_ranges: &[],
});
let palette_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(&palette_pipeline_layout),
vertex: wgpu::VertexState {
module: &palette_shader,
entry_point: "vs_main",
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &palette_shader,
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba8UnormSrgb,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: Default::default(),
depth_stencil: None,
multisample: Default::default(),
multiview: None,
});
PaletteScreenMode {
framebuffer: framebuffer_texture,
palette: palette_texture,
screen_view: screen_texture_view,
bind_group: palette_bind_group,
pipeline: palette_pipeline,
}
}
fn write_framebuffer(&self, queue: &wgpu::Queue, pixels: &[u8]) {
queue.write_texture(
wgpu::ImageCopyTexture {
texture: &self.framebuffer,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&bytemuck::cast_slice(pixels),
wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(320),
rows_per_image: None,
},
wgpu::Extent3d {
width: 320,
height: 240,
depth_or_array_layers: 1,
},
);
}
fn write_palette(&self, queue: &wgpu::Queue, palette: &[u8]) {
queue.write_texture(
wgpu::ImageCopyTexture {
texture: &self.palette,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&bytemuck::cast_slice(palette),
wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(256 * 4),
rows_per_image: None,
},
wgpu::Extent3d {
width: 256,
height: 1,
depth_or_array_layers: 1,
},
);
}
fn resolve_screen(&self, encoder: &mut wgpu::CommandEncoder) {
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: None,
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &self.screen_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
render_pass.set_pipeline(&self.pipeline);
render_pass.set_bind_group(0, &self.bind_group, &[]);
render_pass.draw(0..3, 0..1);
}
}

View File

@@ -0,0 +1,24 @@
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) tex_coords: vec2<f32>,
}
@vertex
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
var out: VertexOutput;
let x = (1.0 - f32(vertex_index)) * 3.0;
let y = f32(vertex_index & 1u) * 3.0 - 1.0;
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
out.tex_coords = vec2<f32>((x + 1.0) * 160.0, (y + 1.0) * 120.0);
return out;
}
@group(0) @binding(0) var framebuffer_texture: texture_2d<u32>;
@group(0) @binding(1) var palette_texture: texture_1d<f32>;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let texel = vec2<i32>(floor(in.tex_coords));
let index = textureLoad(framebuffer_texture, texel, 0).r;
return textureLoad(palette_texture, i32(index), 0);
}

View File

@@ -0,0 +1,157 @@
use wgpu::util::DeviceExt;
use winit::dpi::PhysicalSize;
use super::Filter;
pub struct SquareFilter {
uniform_buffer: wgpu::Buffer,
bind_group: wgpu::BindGroup,
pipeline: wgpu::RenderPipeline,
}
impl SquareFilter {
pub fn new(
device: &wgpu::Device,
screen: &wgpu::TextureView,
resolution: PhysicalSize<u32>,
surface_format: wgpu::TextureFormat,
) -> SquareFilter {
let uniforms = Uniforms {
texture_scale: texture_scale_from_resolution(resolution),
};
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: None,
contents: bytemuck::cast_slice(&[uniforms]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
label: None,
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
mag_filter: wgpu::FilterMode::Linear,
..Default::default()
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&screen),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: uniform_buffer.as_entire_binding(),
},
],
label: None,
});
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(include_str!("square.wgsl").into()),
});
let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format: surface_format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: Default::default(),
depth_stencil: None,
multisample: Default::default(),
multiview: None,
});
SquareFilter {
uniform_buffer,
bind_group,
pipeline: render_pipeline,
}
}
}
impl Filter for SquareFilter {
fn resize(&mut self, queue: &wgpu::Queue, new_size: PhysicalSize<u32>) {
let uniforms = Uniforms {
texture_scale: texture_scale_from_resolution(new_size),
};
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
}
fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {
render_pass.set_pipeline(&self.pipeline);
render_pass.set_bind_group(0, &self.bind_group, &[]);
render_pass.draw(0..6, 0..1);
}
}
fn texture_scale_from_resolution(res: PhysicalSize<u32>) -> [f32; 4] {
let scale = ((res.width as f32) / 160.0).min((res.height as f32) / 120.0);
[
scale / res.width as f32,
scale / res.height as f32,
2.0 / scale,
0.0,
]
}
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct Uniforms {
texture_scale: [f32; 4],
}

View File

@@ -0,0 +1,44 @@
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) tex_coords: vec2<f32>,
}
struct Uniforms {
texture_scale: vec4<f32>,
}
@group(0) @binding(2) var<uniform> uniforms: Uniforms;
@vertex
fn vs_main(
@builtin(vertex_index) in_vertex_index: u32,
) -> VertexOutput {
var out: VertexOutput;
let i = in_vertex_index / 3u + in_vertex_index % 3u;
let x = 0.0 + f32(i % 2u) * 320.0;
let y = 0.0 + f32(i / 2u) * 240.0;
out.clip_position = vec4<f32>((vec2<f32>(x, y) - vec2<f32>(160.0, 120.0)) * uniforms.texture_scale.xy, 0.0, 1.0);
out.tex_coords = vec2<f32>(x, y);
return out;
}
@group(0) @binding(0) var screen_texture: texture_2d<f32>;
@group(0) @binding(1) var linear_sampler: sampler;
fn aa_tex_coord(c: f32) -> f32 {
let low = c - uniforms.texture_scale.z * 0.5;
let high = c + uniforms.texture_scale.z * 0.5;
let base = floor(low);
let center = base + 0.5;
let next = base + 1.0;
if high > next {
return center + (high - next) / (high - low);
} else {
return center;
}
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return textureSample(screen_texture, linear_sampler, vec2<f32>(aa_tex_coord(in.tex_coords.x), aa_tex_coord(in.tex_coords.y)) / vec2<f32>(320.0, 240.0));
}

124
uw8-window/src/lib.rs Normal file
View File

@@ -0,0 +1,124 @@
use anyhow::Result;
use std::time::Instant;
mod cpu;
mod gpu;
pub struct Window {
inner: Box<dyn WindowImpl>,
fps_counter: Option<FpsCounter>,
}
struct FpsCounter {
start: Instant,
num_frames: u32,
}
impl Window {
pub fn new(mut config: WindowConfig) -> Result<Window> {
let fps_counter = if config.fps_counter {
Some(FpsCounter {
start: Instant::now(),
num_frames: 0,
})
} else {
None
};
config.scale = config.scale.max(1.).min(20.);
if config.enable_gpu {
match gpu::Window::new(config) {
Ok(window) => {
return Ok(Window {
inner: Box::new(window),
fps_counter,
})
}
Err(err) => eprintln!(
"Failed to create gpu window: {}\nFalling back tp cpu window",
err
),
}
}
cpu::Window::new().map(|window| Window {
inner: Box::new(window),
fps_counter,
})
}
pub fn begin_frame(&mut self) -> Input {
self.inner.begin_frame()
}
pub fn end_frame(&mut self, framebuffer: &[u8], palette: &[u8], next_frame: Instant) {
self.inner.end_frame(framebuffer, palette, next_frame);
if let Some(ref mut fps_counter) = self.fps_counter {
fps_counter.num_frames += 1;
let elapsed = fps_counter.start.elapsed().as_secs_f32();
if elapsed >= 1.0 {
println!("fps: {:.1}", fps_counter.num_frames as f32 / elapsed);
fps_counter.num_frames = 0;
fps_counter.start = Instant::now();
}
}
}
pub fn is_open(&self) -> bool {
self.inner.is_open()
}
}
#[derive(Debug)]
pub struct WindowConfig {
enable_gpu: bool,
filter: u32,
fullscreen: bool,
fps_counter: bool,
scale: f32,
}
impl Default for WindowConfig {
fn default() -> WindowConfig {
WindowConfig {
enable_gpu: true,
filter: 5,
fullscreen: false,
fps_counter: false,
scale: 2.,
}
}
}
impl WindowConfig {
pub fn parse_arguments(&mut self, args: &mut pico_args::Arguments) {
self.enable_gpu = !args.contains("--no-gpu");
if let Some(filter) = args.opt_value_from_str::<_, String>("--filter").unwrap() {
self.filter = match filter.as_str() {
"1" | "nearest" => 1,
"2" | "fast_crt" => 2,
"3" | "ss_crt" => 3,
"4" | "chromatic" => 4,
"5" | "auto_crt" => 5,
o => {
println!("Unknown --filter '{}'", o);
std::process::exit(1);
}
}
}
self.fullscreen = args.contains("--fullscreen");
self.fps_counter = args.contains("--fps");
self.scale = args
.opt_value_from_str("--scale")
.unwrap()
.unwrap_or(self.scale);
}
}
pub struct Input {
pub gamepads: [u8; 4],
pub reset: bool,
}
trait WindowImpl {
fn begin_frame(&mut self) -> Input;
fn end_frame(&mut self, framebuffer: &[u8], palette: &[u8], next_frame: Instant);
fn is_open(&self) -> bool;
}

59
uw8-window/src/main.rs Normal file
View File

@@ -0,0 +1,59 @@
use std::time::Instant;
use uw8_window::WindowConfig;
fn main() {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let mut args = pico_args::Arguments::from_env();
let mut framebuffer = vec![0u8; 320 * 240];
let mut start_time = Instant::now();
let mut palette = vec![0u32; 256];
for i in 0..256 {
let v = i & 15;
let r = ((i >> 2) & 12) * v;
let g = ((i >> 3) & 12) * v;
let b = ((i >> 4) & 12) * v;
palette[i as usize] = r + (g << 8) + (b << 16);
}
let mut fps_start = Instant::now();
let mut fps_counter = 0;
let mut window_config = WindowConfig::default();
window_config.parse_arguments(&mut args);
let mut window = uw8_window::Window::new(window_config).unwrap();
while window.is_open() {
let input = window.begin_frame();
if input.reset {
start_time = Instant::now();
}
draw_frame(&mut framebuffer, start_time.elapsed().as_secs_f32());
window.end_frame(&framebuffer, bytemuck::cast_slice(&palette), Instant::now());
fps_counter += 1;
let elapsed = fps_start.elapsed().as_secs_f32();
if elapsed >= 1.0 {
println!("{:.1} fps", fps_counter as f32 / elapsed);
fps_start = Instant::now();
fps_counter = 0;
}
}
}
fn draw_frame(framebuffer: &mut [u8], time: f32) {
for x in 0..320 {
let xr = x as f32 - 160.0;
for y in 0..240 {
let yr = y as f32 - 120.0;
let f = 8192.0 / (xr * xr + yr * yr);
let u = xr * f + 512.0 + time * 32.0;
let v = yr * f + time * 29.0;
let c = (u.floor() as i32 ^ v.floor() as i32) as u32;
framebuffer[x + y * 320] = c as u8;
}
}
}

57
web/opus-repro.html Normal file
View File

@@ -0,0 +1,57 @@
<html>
<button onclick="go()">Go!</button>
<canvas id="screen" width="320" height="240"></canvas>
<video id="video"></video>
<script>
function go() {
let audioContext = new AudioContext({sampleRate: 44100});
let oscillator = new OscillatorNode(audioContext);
let gain = new GainNode(audioContext, {gain: 1});
oscillator.connect(gain);
gain.connect(audioContext.destination);
for(let i = 0; i < 8; ++i ) {
gain.gain.setValueAtTime(1, i / 2);
gain.gain.setValueAtTime(0, i / 2 + 0.3);
}
oscillator.start();
oscillator.stop(4);
let screen = document.getElementById('screen');
let context = screen.getContext('2d');
let startTime = Date.now();
let drawFrame = () => {
let time = Date.now() - startTime;
context.fillStyle = 'white';
context.fillRect(0, 0, 320, 240);
if(time < 4000) {
if(time % 500 < 300) {
context.fillStyle = 'black';
context.fillRect(time / 15, 50, 50, 50);
}
window.requestAnimationFrame(drawFrame);
}
};
drawFrame();
let stream = screen.captureStream();
let audioStreamNode = audioContext.createMediaStreamDestination();
gain.connect(audioStreamNode);
stream.addTrack(audioStreamNode.stream.getAudioTracks()[0]);
let recorder = new MediaRecorder(stream, { mimeType: 'video/webm' });
let chunks = [];
recorder.ondataavailable = e => chunks.push(e.data);
recorder.onstop = () => {
let blob = new Blob(chunks, {type: 'video/webm'});
let url = URL.createObjectURL(blob);
let video = document.getElementById('video');
video.src = url;
video.play();
};
recorder.start();
setTimeout(() => recorder.stop(), 4000);
}
</script>
</html>

2
web/run Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
rm -rf .parcel-cache && yarn parcel src/index.html

94
web/src/audiolet.js Normal file
View File

@@ -0,0 +1,94 @@
let U8 = (...a) => new Uint8Array(...a);
class APU extends AudioWorkletProcessor {
constructor() {
super();
this.sampleIndex = 0;
this.currentTime = 0;
this.isFirstMessage = true;
this.pendingUpdates = [];
this.port.onmessage = (ev) => {
if(this.memory) {
if(this.isFirstMessage)
{
this.currentTime += (ev.data.t - this.currentTime) / 8;
this.isFirstMessage = false;
}
this.pendingUpdates.push(ev.data);
} else {
this.load(ev.data[0], ev.data[1]);
}
};
}
async load(platform_data, data) {
let memory = new WebAssembly.Memory({ initial: 4, maximum: 4 });
let importObject = {
env: {
memory
},
};
for (let n of ['acos', 'asin', 'atan', 'atan2', 'cos', 'exp', 'log', 'sin', 'tan', 'pow']) {
importObject.env[n] = Math[n];
}
for (let i = 9; i < 64; ++i) {
importObject.env['reserved' + i] = () => { };
}
let logLine = '';
importObject.env['logChar'] = (c) => {
if(c == 10) {
console.log(logLine);
logLine = '';
} else {
logLine += String.fromCharCode(c);
}
};
for (let i = 0; i < 16; ++i) {
importObject.env['g_reserved' + i] = 0;
}
let instantiate = async (data) => (await WebAssembly.instantiate(data, importObject)).instance;
let platform_instance = await instantiate(platform_data);
for (let name in platform_instance.exports) {
importObject.env[name] = platform_instance.exports[name]
}
let instance = await instantiate(data);
this.memory = memory;
this.snd = instance.exports.snd || platform_instance.exports.sndGes;
this.port.postMessage(2);
}
process(inputs, outputs, parameters) {
this.isFirstMessage = true;
if(this.snd) {
while(this.pendingUpdates.length > 0 && this.pendingUpdates[0].t <= this.currentTime) {
U8(this.memory.buffer, 80, 32).set(U8(this.pendingUpdates.shift().r));
}
let u32Mem = new Uint32Array(this.memory.buffer);
u32Mem[16] = this.currentTime;
let channels = outputs[0];
let index = this.sampleIndex;
let numSamples = channels[0].length;
for(let i = 0; i < numSamples; ++i) {
channels[0][i] = this.snd(index++);
channels[1][i] = this.snd(index++);
}
this.sampleIndex = index & 0xffffffff;
this.currentTime += numSamples / 44.1;
}
return true;
}
}
registerProcessor('apu', APU);

View File

@@ -10,11 +10,12 @@
</head> </head>
<body> <body>
<div id="uw8"> <div id="uw8">
<a href="https://exoticorn.github.io/microw8">MicroW8</a> 0.1.1 <a href="https://exoticorn.github.io/microw8">MicroW8</a> 0.2.2
</div> </div>
<div id="centered"> <div id="centered">
<canvas id="screen" width="320" height="240"> <canvas class="screen" id="screen" width="320" height="240">
</canvas> </canvas>
<button class="screen" id="start" style="display:none">Click to start</button>
<div id="timer" hidden="true"></div> <div id="timer" hidden="true"></div>
<div id="message"></div> <div id="message"></div>
<button id="cartButton" style="visibility:hidden">Load cart...</button> <button id="cartButton" style="visibility:hidden">Load cart...</button>

View File

@@ -12,6 +12,7 @@ let uw8 = MicroW8(document.getElementById('screen'), {
setMessage, setMessage,
keyboardElement: window, keyboardElement: window,
timerElement: document.getElementById("timer"), timerElement: document.getElementById("timer"),
startButton: document.getElementById("start")
}); });
function runModuleFromHash() { function runModuleFromHash() {
@@ -79,7 +80,9 @@ if(location.hash.length != 0) {
url += 'cart.uw8'; url += 'cart.uw8';
} }
try { try {
await uw8.runModuleFromURL(url, true); if(!await uw8.runModuleFromURL(url, true)) {
setupLoad();
}
} catch(e) { } catch(e) {
setupLoad(); setupLoad();
} }

View File

@@ -1,5 +1,15 @@
import loaderUrl from "data-url:../../platform/bin/loader.wasm"; import loaderUrl from "data-url:../../platform/bin/loader.wasm";
import platformUrl from "data-url:../../platform/bin/platform.uw8"; import platformUrl from "data-url:../../platform/bin/platform.uw8";
import audioWorkletUrl from "data-url:./audiolet.js";
class AudioNode extends AudioWorkletNode {
constructor(context) {
super(context, 'apu', {outputChannelCount: [2]});
}
}
let U8 = (...a) => new Uint8Array(...a);
let U32 = (...a) => new Uint32Array(...a);
export default function MicroW8(screen, config = {}) { export default function MicroW8(screen, config = {}) {
if(!config.setMessage) { if(!config.setMessage) {
@@ -18,9 +28,6 @@ export default function MicroW8(screen, config = {}) {
let currentData; let currentData;
let U8 = (d) => new Uint8Array(d);
let U32 = (d) => new Uint32Array(d);
let pad = 0; let pad = 0;
let keyboardElement = config.keyboardElement == undefined ? screen : config.keyboardElement; let keyboardElement = config.keyboardElement == undefined ? screen : config.keyboardElement;
if(keyboardElement) { if(keyboardElement) {
@@ -84,12 +91,24 @@ export default function MicroW8(screen, config = {}) {
keyboardElement.onkeyup = keyHandler; keyboardElement.onkeyup = keyHandler;
} }
let audioContext;
let audioNode;
async function runModule(data, keepUrl) { async function runModule(data, keepUrl) {
if (cancelFunction) { if (cancelFunction) {
cancelFunction(); cancelFunction();
cancelFunction = null; cancelFunction = null;
} }
audioContext = new AudioContext({sampleRate: 44100});
let keepRunning = true;
let abortController = new AbortController();
cancelFunction = () => {
audioContext.close();
keepRunning = false;
abortController.abort();
};
let cartridgeSize = data.byteLength; let cartridgeSize = data.byteLength;
config.setMessage(cartridgeSize); config.setMessage(cartridgeSize);
@@ -97,6 +116,39 @@ export default function MicroW8(screen, config = {}) {
return; return;
} }
await audioContext.audioWorklet.addModule(audioWorkletUrl);
audioNode = new AudioNode(audioContext);
let audioReadyFlags = 0;
let audioReadyResolve;
let audioReadyPromise = new Promise(resolve => audioReadyResolve = resolve);
let updateAudioReady = (f) => {
audioReadyFlags |= f;
if(audioReadyFlags == 3 && audioReadyResolve) {
audioReadyResolve(true);
audioReadyResolve = null;
}
};
let audioStateChange = () => {
if(audioContext.state == 'suspended') {
if(config.startButton) {
config.startButton.style = '';
screen.style = 'display:none';
}
(config.startButton || screen).onclick = () => {
audioContext.resume();
};
} else {
if(config.startButton) {
config.startButton.style = 'display:none';
screen.style = '';
}
updateAudioReady(1);
}
};
audioContext.onstatechange = audioStateChange;
audioStateChange();
currentData = data; currentData = data;
let newURL = window.location.pathname; let newURL = window.location.pathname;
@@ -119,7 +171,7 @@ export default function MicroW8(screen, config = {}) {
if(!devkitMode) { if(!devkitMode) {
memSize.maximum = 4; memSize.maximum = 4;
} }
let memory = new WebAssembly.Memory({ initial: 4, maximum: devkitMode ? 16 : 4 }); let memory = new WebAssembly.Memory(memSize);
let memU8 = U8(memory.buffer); let memU8 = U8(memory.buffer);
let importObject = { let importObject = {
@@ -142,9 +194,9 @@ export default function MicroW8(screen, config = {}) {
let instantiate = async (data) => (await WebAssembly.instantiate(data, importObject)).instance; let instantiate = async (data) => (await WebAssembly.instantiate(data, importObject)).instance;
let loadModuleURL = async (url) => instantiate(loadModuleData(await (await fetch(url)).arrayBuffer())); let loadModuleURL = async (url) => loadModuleData(await (await fetch(url)).arrayBuffer());
loader = await loadModuleURL(loaderUrl); loader = await instantiate(await loadModuleURL(loaderUrl));
for (let n of ['acos', 'asin', 'atan', 'atan2', 'cos', 'exp', 'log', 'sin', 'tan', 'pow']) { for (let n of ['acos', 'asin', 'atan', 'atan2', 'cos', 'exp', 'log', 'sin', 'tan', 'pow']) {
importObject.env[n] = Math[n]; importObject.env[n] = Math[n];
@@ -154,13 +206,28 @@ export default function MicroW8(screen, config = {}) {
importObject.env['reserved' + i] = () => { }; importObject.env['reserved' + i] = () => { };
} }
let logLine = '';
importObject.env['logChar'] = (c) => {
if(c == 10) {
console.log(logLine);
logLine = '';
} else {
logLine += String.fromCharCode(c);
}
};
for (let i = 0; i < 16; ++i) { for (let i = 0; i < 16; ++i) {
importObject.env['g_reserved' + i] = 0; importObject.env['g_reserved' + i] = 0;
} }
data = loadModuleData(data); data = loadModuleData(data);
let platform_instance = await loadModuleURL(platformUrl); let platform_data = await loadModuleURL(platformUrl);
audioNode.port.onmessage = (e) => updateAudioReady(e.data);
audioNode.port.postMessage([platform_data, data]);
let platform_instance = await instantiate(platform_data);
for (let name in platform_instance.exports) { for (let name in platform_instance.exports) {
importObject.env[name] = platform_instance.exports[name] importObject.env[name] = platform_instance.exports[name]
@@ -170,13 +237,35 @@ export default function MicroW8(screen, config = {}) {
let buffer = U32(imageData.data.buffer); let buffer = U32(imageData.data.buffer);
await audioReadyPromise;
let startTime = Date.now(); let startTime = Date.now();
let keepRunning = true;
cancelFunction = () => keepRunning = false;
const timePerFrame = 1000 / 60; const timePerFrame = 1000 / 60;
let nextFrame = startTime;
audioNode.connect(audioContext.destination);
let isPaused = false;
let pauseTime = startTime;
let updateVisibility = isVisible => {
let now = Date.now();
if(isVisible) {
isPaused = false;
audioContext.resume();
startTime += now - pauseTime;
} else {
isPaused = true;
audioContext.suspend();
pauseTime = now;
}
};
window.addEventListener('focus', () => updateVisibility(true), { signal: abortController.signal });
window.addEventListener('blur', () => updateVisibility(false), { signal: abortController.signal });
updateVisibility(document.hasFocus());
if (instance.exports.start) {
instance.exports.start();
}
function mainloop() { function mainloop() {
if (!keepRunning) { if (!keepRunning) {
@@ -184,9 +273,9 @@ export default function MicroW8(screen, config = {}) {
} }
try { try {
let now = Date.now();
let restart = false; let restart = false;
if (now >= nextFrame) { let thisFrame;
if (!isPaused) {
let gamepads = navigator.getGamepads(); let gamepads = navigator.getGamepads();
let gamepad = 0; let gamepad = 0;
for (let i = 0; i < 4; ++i) { for (let i = 0; i < 4; ++i) {
@@ -214,23 +303,36 @@ export default function MicroW8(screen, config = {}) {
} }
let u32Mem = U32(memory.buffer); let u32Mem = U32(memory.buffer);
u32Mem[16] = now - startTime; let time = Date.now() - startTime;
u32Mem[16] = time;
u32Mem[17] = pad | gamepad; u32Mem[17] = pad | gamepad;
if(instance.exports.upd) {
instance.exports.upd(); instance.exports.upd();
}
platform_instance.exports.endFrame(); platform_instance.exports.endFrame();
let palette = U32(memory.buffer.slice(0x13000, 0x13000 + 1024)); let soundRegisters = new ArrayBuffer(32);
U8(soundRegisters).set(U8(memory.buffer, 80, 32));
audioNode.port.postMessage({t: time, r: soundRegisters}, [soundRegisters]);
let palette = U32(memory.buffer, 0x13000, 1024);
for (let i = 0; i < 320 * 240; ++i) { for (let i = 0; i < 320 * 240; ++i) {
buffer[i] = palette[memU8[i + 120]] | 0xff000000; buffer[i] = palette[memU8[i + 120]] | 0xff000000;
} }
canvasCtx.putImageData(imageData, 0, 0); canvasCtx.putImageData(imageData, 0, 0);
nextFrame = Math.max(nextFrame + timePerFrame, now);
let timeOffset = ((time * 6) % 100 - 50) / 6;
thisFrame = startTime + time - timeOffset / 8;
} else {
thisFrame = Date.now();
} }
let now = Date.now();
let nextFrame = Math.max(thisFrame + timePerFrame, now);
if (restart) { if (restart) {
runModule(currentData); runModule(currentData);
} else { } else {
window.requestAnimationFrame(mainloop); window.setTimeout(mainloop, nextFrame - now)
} }
} catch (err) { } catch (err) {
config.setMessage(cartridgeSize, err.toString()); config.setMessage(cartridgeSize, err.toString());
@@ -253,14 +355,25 @@ export default function MicroW8(screen, config = {}) {
let videoRecorder; let videoRecorder;
let videoStartTime; let videoStartTime;
let videoAudioSourceNode;
let videoAudioStreamNode;
function recordVideo() { function recordVideo() {
if(videoRecorder) { if(videoRecorder) {
videoRecorder.stop(); videoRecorder.stop();
videoRecorder = null; videoRecorder = null;
videoAudioSourceNode.disconnect(videoAudioStreamNode);
videoAudioSourceNode = null;
videoAudioStreamNode = null;
return; return;
} }
videoRecorder = new MediaRecorder(screen.captureStream(), { let stream = screen.captureStream();
videoAudioStreamNode = audioContext.createMediaStreamDestination();
videoAudioSourceNode = audioNode;
audioNode.connect(videoAudioStreamNode);
stream.addTrack(videoAudioStreamNode.stream.getAudioTracks()[0]);
videoRecorder = new MediaRecorder(stream, {
mimeType: 'video/webm', mimeType: 'video/webm',
videoBitsPerSecond: 25000000 videoBitsPerSecond: 25000000
}); });
@@ -305,10 +418,11 @@ export default function MicroW8(screen, config = {}) {
async function runModuleFromURL(url, keepUrl) { async function runModuleFromURL(url, keepUrl) {
let response = await fetch(url); let response = await fetch(url);
let type = response.headers.get('Content-Type'); let type = response.headers.get('Content-Type');
if(type && type.includes('html')) { if((type && type.includes('html')) || response.status != 200) {
throw false; return false;
} }
runModule(await response.arrayBuffer(), keepUrl || devkitMode); runModule(await response.arrayBuffer(), keepUrl || devkitMode);
return true;
} }
return { return {

View File

@@ -37,7 +37,7 @@ a:hover {
color: #405040; color: #405040;
} }
#screen { .screen {
width: 320px; width: 320px;
height: 240px; height: 240px;
image-rendering: pixelated; image-rendering: pixelated;
@@ -45,9 +45,16 @@ a:hover {
margin-bottom: 8px; margin-bottom: 8px;
border: 4px solid #303040; border: 4px solid #303040;
box-shadow: 5px 5px 20px black; box-shadow: 5px 5px 20px black;
}
#screen {
cursor: none; cursor: none;
} }
#start {
font-size: 150%;
}
#timer::before { #timer::before {
content: ''; content: '';
display: inline-block; display: inline-block;
@@ -84,21 +91,21 @@ button:active {
} }
@media (min-width: 680px) and (min-height: 620px) { @media (min-width: 680px) and (min-height: 620px) {
#screen { .screen {
width: 640px; width: 640px;
height: 480px; height: 480px;
} }
} }
@media (min-width: 1000px) and (min-height: 800px) { @media (min-width: 1000px) and (min-height: 800px) {
#screen { .screen {
width: 960px; width: 960px;
height: 720px; height: 720px;
} }
} }
@media (width:640px) and (height:480px) { @media (width:640px) and (height:480px) {
#screen { .screen {
width: 640px; width: 640px;
height: 480px; height: 480px;
border: 0; border: 0;

File diff suppressed because it is too large Load Diff