47 Commits

Author SHA1 Message Date
fa089100be backport sound fix 2024-06-07 14:47:30 +02: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
57 changed files with 6616 additions and 1676 deletions

2815
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,24 @@
[package]
name = "uw8"
version = "0.1.2"
version = "0.2.1"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["native", "browser"]
native = ["wasmtime", "minifb", "cpal", "rubato"]
native = ["wasmtime", "uw8-window", "cpal", "rubato" ]
browser = ["warp", "tokio", "tokio-stream", "webbrowser"]
[dependencies]
wasmtime = { version = "0.35.3", optional = true }
wasmtime = { version = "0.37.0", optional = true }
anyhow = "1"
minifb = { version = "0.22", default-features = false, features = ["x11"], optional = true }
env_logger = "0.9"
log = "0.4"
uw8-window = { path = "uw8-window", optional = true }
notify = "4"
pico-args = "0.4"
curlywas = { git = "https://github.com/exoticorn/curlywas.git", rev = "aac7bbd" }
curlywas = { git = "https://github.com/exoticorn/curlywas.git", rev = "0e7ea50" }
wat = "1"
uw8-tool = { path = "uw8-tool" }
same-file = "1"
@@ -25,5 +27,5 @@ tokio = { version = "1.17.0", features = ["sync", "rt"], optional = true }
tokio-stream = { version = "0.1.8", features = ["sync"], optional = true }
webbrowser = { version = "0.6.0", optional = true }
ansi_term = "0.12.1"
cpal = { version = "0.13.5", optional = true }
rubato = { version = "0.11.0", optional = true }
cpal = { version = "0.14.1", optional = true }
rubato = { version = "0.11.0", optional = true }

View File

@@ -43,6 +43,27 @@ Options:
-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.
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>

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

@@ -8,7 +8,7 @@ global mut f: f32 = 2.0;
export fn upd() {
let y: i32;
let inline zero = 0.0;
let inline zero = 0_f;
let lazy control_speed = 0.03125;
s = s + 0.1875 - (f + control_speed) * isButtonPressed(4 <| cls(4)) as f32;
@@ -30,6 +30,8 @@ export fn upd() {
if y == 180 & py > zero {
if x > w | x < zero {
0?80 = 0xc3;
3?80 = 32;
return;
}
py = zero;
@@ -43,6 +45,9 @@ export fn upd() {
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);
0?86 = py < zero;
3?86 = 32 - py as i32;
px = px + (isButtonPressed(3) - isButtonPressed(2)) as f32 * control_speed;
py = py + s;
pz = pz + 1;

View File

@@ -34,6 +34,7 @@ import "env.rectangle_outline" fn rectangle_outline(f32, f32, f32, f32, i32);
import "env.circle_outline" fn circle_outline(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;
const TIME_MS = 0x40;
const GAMEPAD = 0x44;

View File

@@ -34,6 +34,7 @@
(import "env" "circle_outline" (func $circle_outline (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)))
;; to use defines, include this file with a preprocessor
;; like gpp (https://logological.org/gpp).

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
)
)
)

4
platform/Cargo.lock generated
View File

@@ -146,7 +146,7 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "curlywas"
version = "0.1.0"
source = "git+https://github.com/exoticorn/curlywas.git?rev=aac7bbd#aac7bbd8786a26da0dcbe8320b1afefaf6086464"
source = "git+https://github.com/exoticorn/curlywas.git?rev=0e7ea50#0e7ea508cd0e76836283ae68a44c9097df83c8ac"
dependencies = [
"anyhow",
"ariadne",
@@ -391,7 +391,7 @@ checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "upkr"
version = "0.1.0"
source = "git+https://github.com/exoticorn/upkr.git?rev=2e7983fc#2e7983fc650788d98da2eecef2d16f63e849e4a0"
source = "git+https://github.com/exoticorn/upkr.git?rev=d93aec186c9fb91d962c488682a2db125c61306c#d93aec186c9fb91d962c488682a2db125c61306c"
dependencies = [
"anyhow",
"cdivsufsort",

View File

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

Binary file not shown.

Binary file not shown.

View File

@@ -10,7 +10,7 @@ const GesState.Size = GesState.Filter + 8*4;
const GesStateOffset = 32;
const GesBufferOffset = 32 + GesState.Size;
export fn gesSnd(t: i32) -> f32 {
export fn sndGes(t: i32) -> f32 {
let baseAddr = 0!0x12c78;
if !(t & 127) {
let i: i32;
@@ -62,7 +62,6 @@ export fn gesSnd(t: i32) -> f32 {
let phase = channelState!GesChannelState.Phase;
let inline pulseWidth = channelReg?1;
let phaseShift = (pulseWidth - 128) * 255;
let invPhaseInc = 1 as f32 / phaseInc as f32;
i = 0;
@@ -131,7 +130,7 @@ export fn gesSnd(t: i32) -> f32 {
let phaseInc = (freq * (65536.0 / 44100.0)) as i32;
let phase = channelState!GesChannelState.Phase;
if modSrc > ch {
if modSrc < ch {
phase = phase - (phaseInc << 6);
}

View File

@@ -4,6 +4,7 @@ import "env.sin" fn sin(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 {
(0!64) as f32 / 1000 as f32
@@ -32,11 +33,9 @@ export fn random() -> i32 {
}
export fn random64() -> i64 {
let state: i64;
randomState = (state := (
state := randomState ^ (randomState #>> 12i64)
) ^ (state << 25i64)
) ^ (state #>> 27i64);
let lazy state = randomState ^ (randomState #>> 12i64);
let lazy state = state ^ (state << 25i64);
randomState = state ^ (state #>> 27i64);
randomState * 0x2545f4914f6cdd1di64
}
@@ -62,12 +61,8 @@ export fn cls(col: i32) {
let i: i32;
textCursorX = 0;
textCursorY = 0;
graphicsText = 0;
col = (col & 255) * 0x1010101;
loop pixels {
i!120 = col;
branch_if (i := i + 4) < 320*240: pixels;
}
outputChannel = 0;
memory.fill(120, col, 320*240);
}
export fn setPixel(x: i32, y: i32, col: i32) {
@@ -91,12 +86,70 @@ fn clamp(v: i32, min: i32, max: i32) -> i32 {
export fn hline(x1: i32, x2: i32, y: i32, col: i32) {
x1 = clamp(x1, 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 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;
branch_if (ptr := ptr + 1) < end: pixels;
}
}
}
@@ -307,7 +360,7 @@ global mut textCursorX = 0;
global mut textCursorY = 0;
global mut textColor = 15;
global mut bgColor = 0;
global mut graphicsText = 0;
global mut outputChannel = 0;
export fn printChar(char: i32) {
loop chars {
@@ -319,6 +372,20 @@ export fn printChar(char: i32) {
global mut controlCodeLength = 0;
fn printSingleChar(char: i32) {
if char >= 4 & char <= 6 {
outputChannel = char - 4;
if !outputChannel {
textCursorX = 0;
textCursorY = 0;
}
return;
}
if outputChannel >= 2 {
logChar(char);
return;
}
controlCodeLength?0x12d20 = char;
controlCodeLength = controlCodeLength + 1;
char = 0x12d20?0;
@@ -332,13 +399,6 @@ fn printSingleChar(char: i32) {
return;
}
if char == 4 | char == 5 {
graphicsText = char == 5;
textCursorX = 0;
textCursorY = 0;
return;
}
if char == 7 {
80?0 = 80?0 ^ 2;
return;
@@ -346,7 +406,7 @@ fn printSingleChar(char: i32) {
if char == 8 {
textCursorX = textCursorX - 8;
if !graphicsText & textCursorX < 0 {
if !outputChannel & textCursorX < 0 {
textCursorX = 320-8;
printSingleChar(11);
}
@@ -354,7 +414,7 @@ fn printSingleChar(char: i32) {
}
if char == 9 {
if !graphicsText & textCursorX >= 320 {
if !outputChannel & textCursorX >= 320 {
printChar(0xd0a);
}
textCursorX = textCursorX + 8;
@@ -363,7 +423,7 @@ fn printSingleChar(char: i32) {
if char == 10 {
textCursorY = textCursorY + 8;
if !graphicsText & textCursorY >= 240 {
if !outputChannel & textCursorY >= 240 {
textCursorY = 240 - 8;
let i: i32;
loop scroll_copy {
@@ -377,7 +437,7 @@ fn printSingleChar(char: i32) {
if char == 11 {
textCursorY = textCursorY - 8;
if !graphicsText & textCursorY < 0 {
if !outputChannel & textCursorY < 0 {
textCursorY = 0;
let i = 320 * (240 - 8);
loop scroll_copy {
@@ -417,8 +477,8 @@ fn printSingleChar(char: i32) {
}
if char == 31 {
textCursorX = 0x12d20?1 * (8 - graphicsText * 6);
textCursorY = 0x12d20?2 * (8 - graphicsText * 7);
textCursorX = 0x12d20?1 * (8 - outputChannel * 6);
textCursorY = 0x12d20?2 * (8 - outputChannel * 7);
return;
}
@@ -443,7 +503,7 @@ data(0x12d00) {
}
fn drawChar(char: i32) {
if !graphicsText & textCursorX >= 320 {
if !outputChannel & textCursorX >= 320 {
printChar(0xd0a);
}
@@ -451,7 +511,7 @@ fn drawChar(char: i32) {
loop rows {
let bits = (char * 8 + y)?0x13400;
let x = 0;
if graphicsText {
if outputChannel {
loop pixels {
if (bits := bits << 1) & 256 {
setPixel(textCursorX + x, textCursorY + y, textColor);
@@ -503,7 +563,7 @@ export fn setBackgroundColor(col: 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;
textCursorY = y * scale;
}
@@ -542,13 +602,6 @@ export fn endFrame() {
68!4 = 68!0;
}
fn memclr(base: i32, size: i32) {
loop bytes {
(base + (size := size - 1))?0 = 0;
branch_if size: bytes;
}
}
start fn setup() {
let i: i32 = 12*16*3-1;
let avg: f32;
@@ -581,9 +634,9 @@ start fn setup() {
branch_if (i := i - 1) >= 0: expand_sweetie;
}
memclr(0, 64);
memclr(112, 8);
memclr(0x14000, 0x2c000);
memory.fill(0, 0, 64);
memory.fill(112, 0, 8);
memory.fill(0x14000, 0, 0x2c000);
cls(0);

View File

@@ -15,11 +15,11 @@ The initial motivation behind MicroW8 was to explore whether there was a way to
* Gamepad input (D-Pad + 4 Buttons)
## Examples
* [Fireworks](v0.1.2#AgwvgP+M59snqjl4CMKw5sqm1Zw9yJCbSviMjeLUdHus2a3yl/a99+uiBeqZgP/2jqSjrLjRk73COMM6OSLpsxK8ugT1kuk/q4hQUqqPpGozHoa0laulzGGcahzdfdJsYaK1sIdeIYS9M5PnJx/Wk9H+PvWEPy2Zvv7I6IW7Fg==) (127 bytes): Some fireworks to welcome 2022.
* [Skip Ahead](v0.1.2#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
* [OhNoAnotherTunnel](v0.1.2#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.2#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.2#At/p39+IBnj6ry1TRe7jzVy2A4tXgBvmoW2itzoyF2aM28pGy5QDiKxqrk8l9sbWZLtnAb+jgOfU+9QhpuyCAkhN6gPOU481IUL/df96vNe3h288Dqwhd3sfFpothIVFsMwRK72kW2hiR7zWsaXyy5pNmjR6BJk4piWx9ApT1ZwoUajhk6/zij6itq/FD1U3jj/J3MOwqZ2ef8Bv6ZPQlJIYVf62icGa69wS6SI1qBpIFiF14F8PcztRVbKIxLpT4ArCS6nz6FPnyUkqATGSBNPJ): Just a simple viewer for the default font and palette.
* [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
* [Fireworks](v0.2.0#AgwvgP+M59snqjl4CMKw5sqm1Zw9yJCbSviMjeLUdHus2a3yl/a99+uiBeqZgP/2jqSjrLjRk73COMM6OSLpsxK8ugT1kuk/q4hQUqqPpGozHoa0laulzGGcahzdfdJsYaK1sIdeIYS9M5PnJx/Wk9H+PvWEPy2Zvv7I6IW7Fg==) (127 bytes): Some fireworks to welcome 2022.
* [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.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.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:
@@ -29,19 +29,36 @@ Examplers for older versions:
## Versions
### v0.1.2
### v0.2.1
* [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)
* [Web runtime](v0.2.1)
* [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.2.1/microw8-0.2.1-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.2.1/microw8-0.2.1-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
* 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

View File

@@ -225,30 +225,45 @@ Moving/printing past any border does not cause any special operation, the cursor
Characters 0-31 are control characters and don't print by default. They take the next 0-2 following characters as parameters.
Avoid the reserved control chars, they are currently NOPs but their behavior can change in later MicroW8 versions.
| Code | Parameters | Operation |
| ----- | ---------- | ------------------------------------ |
| 0 | - | Nop |
| 1 | char | Print char (including control chars) |
| 2-3 | - | Reserved |
| 4 | - | Switch to normal mode |
| 5 | - | Switch to graphics mode |
| 6 | - | Reserved |
| 7 | - | Bell / trigger sound channel 0 |
| 8 | - | Move cursor left |
| 9 | - | Move cursor right |
| 10 | - | Move cursor down |
| 11 | - | Move cursor up |
| 12 | - | do `cls(background_color)` |
| 13 | - | Move cursor to the left border |
| 14 | color | Set the background color |
| 15 | color | Set the text color |
| 16-23 | - | Reserved |
| 24 | - | Swap text/background colors |
| 25-30 | - | Reserved |
| 31 | x, y | Set cursor position (*) |
| Code | Parameters | Operation |
| ----- | ---------- | ------------------------------------------ |
| 0 | - | Nop |
| 1 | char | Print char (including control chars) |
| 2-3 | - | Reserved |
| 4 | - | Switch to normal mode, reset cursor to 0,0 |
| 5 | - | Switch to graphics mode |
| 6 | - | Switch output to (debug) console |
| 7 | - | Bell / trigger sound channel 0 |
| 8 | - | Move cursor left |
| 9 | - | Move cursor right |
| 10 | - | Move cursor down |
| 11 | - | Move cursor up |
| 12 | - | do `cls(background_color)` |
| 13 | - | Move cursor to the left border |
| 14 | color | Set the background color |
| 15 | color | Set the text color |
| 16-23 | - | Reserved |
| 24 | - | Swap text/background colors |
| 25-30 | - | Reserved |
| 31 | x, y | Set cursor position (*) |
(*) 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)
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.
@@ -421,6 +436,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.
* `-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`
Usage:

View File

@@ -2,6 +2,69 @@
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)

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>
<h1 class="text-center heading-text">A WebAssembly based fantasy console</h1>
</section>
<a href="v0.1.2">
<a href="v0.2.1">
<img class="demonstration-gif" style="width:640px;height:480px;image-rendering:pixelated" src="img/technotunnel.png"></img>
</a>
</div>

View File

@@ -14,9 +14,6 @@ use anyhow::Result;
pub trait Runtime {
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 run_frame(&mut self) -> Result<()>;
}
}

View File

@@ -13,6 +13,7 @@ use uw8::RunWebServer;
use uw8::Runtime;
fn main() -> Result<()> {
env_logger::Builder::from_env(env_logger::Env::default()).init();
let mut args = Arguments::from_env();
// try to enable ansi support in win10 cmd shell
@@ -52,6 +53,7 @@ fn main() -> Result<()> {
#[cfg(any(feature = "native", feature = "browser"))]
fn run(mut args: Arguments) -> Result<()> {
let watch_mode = args.contains(["-w", "--watch"]);
#[allow(unused)]
let timeout: Option<u32> = args.opt_value_from_str(["-t", "--timeout"])?;
let mut config = Config::default();
@@ -79,7 +81,17 @@ fn run(mut args: Arguments) -> Result<()> {
#[cfg(not(feature = "native"))]
let run_browser = args.contains(["-b", "--browser"]) || true;
let disable_audio = args.contains(["-m", "--disable-audio"]);
#[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()))?;
@@ -92,7 +104,7 @@ fn run(mut args: Arguments) -> Result<()> {
unimplemented!();
#[cfg(feature = "native")]
{
let mut microw8 = MicroW8::new()?;
let mut microw8 = MicroW8::new(timeout, window_config)?;
if disable_audio {
microw8.disable_audio();
}
@@ -105,10 +117,6 @@ fn run(mut args: Arguments) -> Result<()> {
Box::new(RunWebServer::new())
};
if let Some(timeout) = timeout {
runtime.set_timeout(timeout);
}
let mut first_run = true;
while runtime.is_open() {
@@ -167,7 +175,11 @@ fn load_cart(filename: &Path, config: &Config) -> (Result<Vec<u8>>, Vec<PathBuf>
if let Some(ref pack_config) = config.pack {
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 {

File diff suppressed because one or more lines are too long

View File

@@ -4,31 +4,21 @@ use std::{thread, time::Instant};
use anyhow::{anyhow, Result};
use cpal::traits::*;
use minifb::{Key, Window, WindowOptions};
use rubato::Resampler;
use uw8_window::{Window, WindowConfig};
use wasmtime::{
Engine, 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 {
window: Window,
stream: Option<cpal::Stream>,
engine: Engine,
loader_module: Module,
window: Window,
window_buffer: Vec<u32>,
instance: Option<UW8Instance>,
timeout: u32,
disable_audio: bool,
module_data: Option<Vec<u8>>,
timeout: u32,
instance: Option<UW8Instance>,
}
struct UW8Instance {
@@ -37,9 +27,8 @@ struct UW8Instance {
end_frame: TypedFunc<(), ()>,
update: Option<TypedFunc<(), ()>>,
start_time: Instant,
module: Vec<u8>,
watchdog: Arc<Mutex<UW8WatchDog>>,
sound: Option<Uw8Sound>,
sound_tx: Option<mpsc::SyncSender<RegisterUpdate>>,
}
impl Drop for UW8Instance {
@@ -51,45 +40,36 @@ impl Drop for UW8Instance {
}
struct UW8WatchDog {
interupt: wasmtime::InterruptHandle,
timeout: u32,
engine: Engine,
stop: bool,
}
impl MicroW8 {
pub fn new() -> Result<MicroW8> {
let engine = wasmtime::Engine::new(wasmtime::Config::new().interruptable(true))?;
pub fn new(timeout: Option<u32>, window_config: WindowConfig) -> Result<MicroW8> {
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 =
wasmtime::Module::new(&engine, include_bytes!("../platform/bin/loader.wasm"))?;
let options = WindowOptions {
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)));
let window = Window::new(window_config)?;
Ok(MicroW8 {
window,
stream: None,
engine,
loader_module,
window,
window_buffer: vec![0u32; 320 * 240],
instance: None,
timeout: 30,
disable_audio: false,
module_data: None,
timeout: timeout.unwrap_or(0),
instance: None,
})
}
fn reset(&mut self) {
self.instance = None;
for v in &mut self.window_buffer {
*v = 0;
}
}
pub fn disable_audio(&mut self) {
self.disable_audio = true;
}
@@ -97,17 +77,15 @@ impl MicroW8 {
impl super::Runtime for MicroW8 {
fn is_open(&self) -> bool {
self.window.is_open() && !self.window.is_key_down(Key::Escape)
}
fn set_timeout(&mut self, timeout: u32) {
self.timeout = timeout;
self.window.is_open()
}
fn load(&mut self, module_data: &[u8]) -> Result<()> {
self.reset();
self.stream = None;
self.instance = None;
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)))?;
@@ -133,8 +111,7 @@ impl super::Runtime for MicroW8 {
let platform_instance = instantiate_platform(&mut linker, &mut store, &platform_module)?;
let watchdog = Arc::new(Mutex::new(UW8WatchDog {
interupt: store.interrupt_handle()?,
timeout: self.timeout,
engine: self.engine.clone(),
stop: false,
}));
@@ -142,16 +119,11 @@ impl super::Runtime for MicroW8 {
let watchdog = watchdog.clone();
thread::spawn(move || loop {
thread::sleep(Duration::from_millis(17));
if let Ok(mut watchdog) = watchdog.lock() {
if let Ok(watchdog) = watchdog.lock() {
if watchdog.stop {
break;
}
if watchdog.timeout > 0 {
watchdog.timeout -= 1;
if watchdog.timeout == 0 {
watchdog.interupt.interrupt();
}
}
watchdog.engine.increment_epoch();
} else {
break;
}
@@ -159,23 +131,20 @@ impl super::Runtime for MicroW8 {
}
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").ok();
let sound = if self.disable_audio {
None
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)
(Some(sound.tx), Some(sound.stream))
}
Err(err) => {
eprintln!("Failed to init sound: {}", err);
None
(None, None)
}
}
};
@@ -186,44 +155,45 @@ impl super::Runtime for MicroW8 {
end_frame,
update,
start_time: Instant::now(),
module: module_data.into(),
watchdog,
sound,
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 = 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 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(&gamepad.to_le_bytes());
mem[68..72].copy_from_slice(&input.gamepads);
}
if let Ok(mut watchdog) = instance.watchdog.lock() {
watchdog.timeout = self.timeout;
}
instance.store.set_epoch_deadline(self.timeout as u64);
if let Some(ref update) = instance.update {
result = update.call(&mut instance.store, ());
}
if let Ok(mut watchdog) = instance.watchdog.lock() {
watchdog.timeout = 0;
if let Err(err) = update.call(&mut instance.store, ()) {
result = Err(err);
}
}
instance.end_frame.call(&mut instance.store, ())?;
@@ -231,30 +201,23 @@ impl super::Runtime for MicroW8 {
let mut sound_regs = [0u8; 32];
sound_regs.copy_from_slice(&memory[80..112]);
if let Some(ref sound) = instance.sound {
sound.tx.send(sound_regs)?;
if let Some(ref sound_tx) = instance.sound_tx {
let _ = sound_tx.send(RegisterUpdate {
time,
data: sound_regs,
});
}
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;
}
let framebuffer_mem = &memory[120..(120 + 320 * 240)];
let palette_mem = &memory[0x13000..];
self.window
.end_frame(framebuffer_mem, palette_mem, next_frame);
if self.window.is_key_pressed(Key::R, minifb::KeyRepeat::No) {
self.load(&instance.module)?;
} else if result.is_ok() {
if result.is_ok() {
self.instance = Some(instance);
}
}
self.window
.update_with_buffer(&self.window_buffer, 320, 240)?;
result?;
Ok(())
}
@@ -277,6 +240,16 @@ fn add_native_functions(
for i in 10..64 {
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 {
linker.define(
"env",
@@ -312,9 +285,14 @@ fn instantiate_platform(
Ok(platform_instance)
}
struct RegisterUpdate {
time: i32,
data: [u8; 32],
}
struct Uw8Sound {
stream: cpal::Stream,
tx: mpsc::SyncSender<[u8; 32]>,
tx: mpsc::SyncSender<RegisterUpdate>,
}
fn init_sound(
@@ -323,6 +301,7 @@ fn init_sound(
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)))?;
@@ -335,7 +314,7 @@ fn init_sound(
let snd = instance
.get_typed_func::<(i32,), f32, _>(&mut store, "snd")
.or_else(|_| platform_instance.get_typed_func::<(i32,), f32, _>(&mut store, "gesSnd"))?;
.or_else(|_| platform_instance.get_typed_func::<(i32,), f32, _>(&mut store, "sndGes"))?;
let host = cpal::default_host();
let device = host
@@ -363,7 +342,7 @@ fn init_sound(
.ok_or_else(|| anyhow!("Could not find float output config"))?;
let sample_rate = cpal::SampleRate(44100)
.max(config.min_sample_rate())
.max(config.max_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,
@@ -378,9 +357,7 @@ fn init_sound(
let sample_rate = config.sample_rate.0 as usize;
let (tx, rx) = mpsc::sync_channel::<[u8; 32]>(1);
let start_time = Instant::now();
let (tx, rx) = mpsc::sync_channel::<RegisterUpdate>(30);
struct Resampler {
resampler: rubato::FftFixedIn<f32>,
@@ -403,60 +380,102 @@ fn init_sound(
};
let mut sample_index = 0;
let mut pending_updates: Vec<RegisterUpdate> = Vec::with_capacity(30);
let mut current_time = 0;
let stream = device.build_output_stream(
&config,
move |mut buffer: &mut [f32], _| {
if let Ok(regs) = rx.try_recv() {
memory.write(&mut store, 80, &regs).unwrap();
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);
}
{
let time = start_time.elapsed().as_millis() as i32;
let mem = memory.data_mut(&mut store);
mem[64..68].copy_from_slice(&time.to_le_bytes());
}
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();
}
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(snd.call(&mut store, (sample_index,)).unwrap_or(0.0));
resampler.input_buffers[1]
.push(snd.call(&mut store, (sample_index + 1,)).unwrap_or(0.0));
sample_index = sample_index.wrapping_add(2);
}
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());
resampler
.resampler
.process_into_buffer(
&resampler.input_buffers,
&mut resampler.output_buffers,
None,
)
.unwrap();
resampler.output_index = 0;
let mut buffer = &mut outer_buffer[..step_size];
{
let mem = memory.data_mut(&mut store);
mem[64..68].copy_from_slice(&current_time.to_le_bytes());
}
fn clamp_sample(s: f32) -> f32 {
if s.is_nan() {
0.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..];
s.max(-1.0).min(1.0)
}
}
} else {
for v in buffer {
*v = snd.call(&mut store, (sample_index,)).unwrap_or(0.0);
sample_index = sample_index.wrapping_add(1);
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 {
for v in buffer {
*v = clamp_sample(snd.call(&mut store, (sample_index,)).unwrap_or(0.0));
sample_index = sample_index.wrapping_add(1);
}
}
outer_buffer = &mut outer_buffer[step_size..];
current_time =
current_time.wrapping_add((step_size * 500 / sample_rate).max(1) as i32);
}
},
move |err| {

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;
}

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');
}

1
todo.txt Normal file
View File

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

2
uw8-tool/Cargo.lock generated
View File

@@ -189,7 +189,7 @@ checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "upkr"
version = "0.1.0"
source = "git+https://github.com/exoticorn/upkr.git?rev=2e7983fc#2e7983fc650788d98da2eecef2d16f63e849e4a0"
source = "git+https://github.com/exoticorn/upkr.git?rev=d93aec186c9fb91d962c488682a2db125c61306c#d93aec186c9fb91d962c488682a2db125c61306c"
dependencies = [
"anyhow",
"cdivsufsort",

View File

@@ -11,5 +11,5 @@ wasm-encoder = "0.8"
walrus = "0.19"
anyhow = "1"
pico-args = "0.4"
upkr = { git = "https://github.com/exoticorn/upkr.git", rev = "2e7983fc" }
upkr = { git = "https://github.com/exoticorn/upkr.git", rev = "d93aec186c9fb91d962c488682a2db125c61306c" }
pbr = "1"

View File

@@ -167,6 +167,7 @@ impl BaseModule {
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));
for i in functions.len()..64 {
add_function(
@@ -286,7 +287,7 @@ impl BaseModule {
pub fn create_binary(path: &Path) -> Result<()> {
let base1 = BaseModule::for_format_version(1)?.to_wasm();
let data = upkr::pack(&base1, 4, None);
let data = upkr::pack(&base1, 4, false, None);
File::create(path)?.write_all(&data)?;
Ok(())
}

View File

@@ -1,7 +1,15 @@
mod base_module;
mod pack;
mod filter_exports;
mod pack;
pub use base_module::BaseModule;
pub use pack::{pack, pack_file, unpack, unpack_file, PackConfig};
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

@@ -63,6 +63,7 @@ pub fn pack(data: &[u8], config: &PackConfig) -> Result<Vec<u8>> {
uw8.extend_from_slice(&upkr::pack(
&result[8..],
level,
false,
Some(&mut |pos| {
pb.set(pos as u64);
}),
@@ -89,7 +90,7 @@ pub fn unpack(data: Vec<u8>) -> Result<Vec<u8>> {
let (version, data) = match data[0] {
0 => return Ok(data),
1 => (1, data[1..].to_vec()),
2 => (1, upkr::unpack(&data[1..])),
2 => (1, upkr::unpack(&data[1..], false)),
other => bail!("Uknown format version {}", other),
};
@@ -219,6 +220,11 @@ impl<'a> ParsedModule<'a> {
validate_table_section(reader)?;
table_section = Some(Section::new(range, ()));
}
Payload::MemorySection(reader) => {
if reader.get_count() != 0 {
bail!("Found non-empty MemorySection. Memory has to be imported!");
}
}
Payload::ElementSection(mut reader) => {
let mut elements = Vec::with_capacity(reader.get_count() as usize);
for _ in 0..reader.get_count() {
@@ -962,6 +968,8 @@ fn remap_function(
De::I64TruncSatF32U => En::I64TruncSatF32U,
De::I64TruncSatF64S => En::I64TruncSatF64S,
De::I64TruncSatF64U => En::I64TruncSatF64U,
De::MemoryCopy { src, dst } => En::MemoryCopy { src, dst },
De::MemoryFill { mem } => En::MemoryFill(mem),
other => bail!("Unsupported instruction {:?}", other),
});
}

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

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

1704
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.26.1"
env_logger = "0.9"
log = "0.4"
pico-args = "0.4"
wgpu = "0.13.1"
pollster = "0.2"
bytemuck = { version = "1.4", features = [ "derive" ] }
anyhow = "1"
minifb = { version = "0.23.0", default-features = false, features = ["x11"] }
winapi = { version = "0.3.9", features = ["std"] }

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 pixel = floor(in.tex_coords);
let o = vec2<f32>(0.5) - (in.tex_coords - pixel);
let pixel = vec2<i32>(pixel);
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;
let offset_x0 = max(abs(offset_x + vec4<f32>(-1.0)) - vec4<f32>(0.5), vec4<f32>(0.0));
let offset_x1 = max(abs(offset_x) - vec4<f32>(0.5), vec4<f32>(0.0));
let offset_x2 = max(abs(offset_x + vec4<f32>(1.0)) - vec4<f32>(0.5), vec4<f32>(0.0));
let offset_x0 = offset_x0 * offset_x0;
let offset_x1 = offset_x1 * offset_x1;
let offset_x2 = offset_x2 * offset_x2;
let offset_yr = offset_y + vec4<f32>(-1.0);
let 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);
let 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);
let offset_yr = offset_y + vec4<f32>(1.0);
let 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 offset = max(0.0, abs(offset) - 0.4);
return 1.0 / (1.0 + offset * offset * 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);
}

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

@@ -0,0 +1,545 @@
use crate::{Input, WindowConfig, WindowImpl};
use anyhow::{anyhow, Result};
use std::{num::NonZeroU32, time::Instant};
use winit::{
dpi::PhysicalSize,
event::{Event, VirtualKeyCode, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::{Fullscreen, WindowBuilder},
};
use winit::platform::run_return::EventLoopExtRunReturn;
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,
_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: 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(640u32, 480))
.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 instance = wgpu::Instance::new(wgpu::Backends::all());
let surface = unsafe { instance.create_surface(&window) };
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 {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface.get_supported_formats(&adapter)[0],
width: window.inner_size().width,
height: window.inner_size().height,
present_mode: wgpu::PresentMode::AutoNoVsync,
};
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.run_return(|event, _, control_flow| {
*control_flow = ControlFlow::WaitUntil(self.next_frame);
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 => {
self.is_open = false;
*control_flow = ControlFlow::Exit;
}
WindowEvent::KeyboardInput { input, .. } => {
fn gamepad_button(input: &winit::event::KeyboardInput) -> u8 {
match input.scancode {
44 => 16,
45 => 32,
30 => 64,
31 => 128,
_ => match input.virtual_keycode {
Some(VirtualKeyCode::Up) => 1,
Some(VirtualKeyCode::Down) => 2,
Some(VirtualKeyCode::Left) => 4,
Some(VirtualKeyCode::Right) => 8,
_ => 0,
},
}
}
if input.state == winit::event::ElementState::Pressed {
match input.virtual_keycode {
Some(VirtualKeyCode::Escape) => {
self.is_open = false;
*control_flow = ControlFlow::Exit;
}
Some(VirtualKeyCode::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);
}
Some(VirtualKeyCode::R) => reset = true,
Some(VirtualKeyCode::Key1) => new_filter = Some(1),
Some(VirtualKeyCode::Key2) => new_filter = Some(2),
Some(VirtualKeyCode::Key3) => new_filter = Some(3),
Some(VirtualKeyCode::Key4) => new_filter = Some(4),
Some(VirtualKeyCode::Key5) => new_filter = Some(5),
_ => (),
}
self.gamepads[0] |= gamepad_button(&input);
} else {
self.gamepads[0] &= !gamepad_button(&input);
}
}
_ => (),
},
Event::RedrawEventsCleared => {
if Instant::now() >= self.next_frame
// workaround needed on Wayland until the next winit release
&& self.window.fullscreen().is_some() == self.is_fullscreen
{
*control_flow = ControlFlow::Exit
}
}
_ => (),
}
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,
);
}
});
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: true,
},
})],
depth_stencil_attachment: 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,
});
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,
});
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,
});
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: NonZeroU32::new(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: NonZeroU32::new(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: true,
},
})],
depth_stencil_attachment: 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));
}

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

@@ -0,0 +1,117 @@
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(config: WindowConfig) -> Result<Window> {
let fps_counter = if config.fps_counter {
Some(FpsCounter {
start: Instant::now(),
num_frames: 0,
})
} else {
None
};
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,
}
impl Default for WindowConfig {
fn default() -> WindowConfig {
WindowConfig {
enable_gpu: true,
filter: 5,
fullscreen: false,
fps_counter: false,
}
}
}
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");
}
}
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>

View File

@@ -3,13 +3,17 @@ 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(isNaN(ev.data)) {
U8(this.memory.buffer, 80, 32).set(U8(ev.data));
} else {
this.startTime = ev.data;
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]);
}
@@ -33,6 +37,16 @@ class APU extends AudioWorkletProcessor {
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;
}
@@ -49,15 +63,19 @@ class APU extends AudioWorkletProcessor {
this.memory = memory;
this.snd = instance.exports.snd || platform_instance.exports.gesSnd;
this.snd = instance.exports.snd || platform_instance.exports.sndGes;
this.port.postMessage(2);
}
process(inputs, outputs, parameters) {
if(this.snd && this.startTime) {
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] = Date.now() - this.startTime;
u32Mem[16] = this.currentTime;
let channels = outputs[0];
let index = this.sampleIndex;
let numSamples = channels[0].length;
@@ -66,6 +84,7 @@ class APU extends AudioWorkletProcessor {
channels[1][i] = this.snd(index++);
}
this.sampleIndex = index & 0xffffffff;
this.currentTime += numSamples / 44.1;
}
return true;

View File

@@ -10,7 +10,7 @@
</head>
<body>
<div id="uw8">
<a href="https://exoticorn.github.io/microw8">MicroW8</a> 0.1.2
<a href="https://exoticorn.github.io/microw8">MicroW8</a> 0.2.1
</div>
<div id="centered">
<canvas class="screen" id="screen" width="320" height="240">

View File

@@ -107,7 +107,7 @@ export default function MicroW8(screen, config = {}) {
audioContext.close();
keepRunning = false;
abortController.abort();
}
};
let cartridgeSize = data.byteLength;
@@ -206,6 +206,16 @@ export default function MicroW8(screen, config = {}) {
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;
}
@@ -232,7 +242,6 @@ export default function MicroW8(screen, config = {}) {
let startTime = Date.now();
const timePerFrame = 1000 / 60;
let nextFrame = startTime;
audioNode.connect(audioContext.destination);
@@ -244,18 +253,16 @@ export default function MicroW8(screen, config = {}) {
isPaused = false;
audioContext.resume();
startTime += now - pauseTime;
audioNode.port.postMessage(startTime);
} else {
isPaused = true;
audioContext.suspend();
pauseTime = now;
audioNode.port.postMessage(0);
}
};
window.addEventListener('focus', () => updateVisibility(true), { signal: abortController.signal });
window.addEventListener('blur', () => updateVisibility(false), { signal: abortController.signal });
updateVisibility(document.hasFocus());
function mainloop() {
if (!keepRunning) {
return;
@@ -263,6 +270,7 @@ export default function MicroW8(screen, config = {}) {
try {
let restart = false;
let thisFrame;
if (!isPaused) {
let gamepads = navigator.getGamepads();
let gamepad = 0;
@@ -291,7 +299,8 @@ export default function MicroW8(screen, config = {}) {
}
let u32Mem = U32(memory.buffer);
u32Mem[16] = Date.now() - startTime;
let time = Date.now() - startTime;
u32Mem[16] = time;
u32Mem[17] = pad | gamepad;
if(instance.exports.upd) {
instance.exports.upd();
@@ -300,21 +309,26 @@ export default function MicroW8(screen, config = {}) {
let soundRegisters = new ArrayBuffer(32);
U8(soundRegisters).set(U8(memory.buffer, 80, 32));
audioNode.port.postMessage(soundRegisters, [soundRegisters]);
audioNode.port.postMessage({t: time, r: soundRegisters}, [soundRegisters]);
let palette = U32(memory.buffer, 0x13000, 1024);
for (let i = 0; i < 320 * 240; ++i) {
buffer[i] = palette[memU8[i + 120]] | 0xff000000;
}
canvasCtx.putImageData(imageData, 0, 0);
let timeOffset = ((time * 6) % 100 - 50) / 6;
thisFrame = startTime + time - timeOffset / 8;
} else {
thisFrame = Date.now();
}
let now = Date.now();
nextFrame = Math.max(nextFrame + timePerFrame, now);
let nextFrame = Math.max(thisFrame + timePerFrame, now);
if (restart) {
runModule(currentData);
} else {
window.setTimeout(mainloop, Math.round(nextFrame - now))
window.setTimeout(mainloop, nextFrame - now)
}
} catch (err) {
config.setMessage(cartridgeSize, err.toString());

File diff suppressed because it is too large Load Diff