49 Commits

Author SHA1 Message Date
53ba21084d implement random walk for enemies 2023-04-10 14:05:06 +02:00
3424976d40 start implementing a pacman-like game in zig 2023-04-10 13:26:58 +02:00
1a06ebbc95 update zig example 2023-04-08 15:10:09 +02:00
d1b6fa36e8 update frame pacing calculation in native runtime 2023-04-07 18:33:22 +02:00
b3a8512129 update frame pacing calculation in web runtime
fixes carts running too slow in chrome
2023-04-07 17:08:04 +02:00
c8a9f533ef copy optimized blitSprite impl to grabSprite, add simple docs 2023-04-03 23:44:12 +02:00
4a1d607bcb optimize blitSprite 2023-04-03 00:09:02 +02:00
bbb75838cd first unoptimized version of blitSprite & grabSprite 2023-04-02 23:21:31 +02:00
dbeb242fb2 add support for br_table instruction when packing cart 2023-03-20 23:08:42 +01:00
4fe4bce0ad token env var has changed 2023-01-31 00:14:25 +01:00
fe86153562 use fixed version of zola-deploy-action 2023-01-31 00:01:07 +01:00
c9dadaca2e try add safe.directory 2023-01-30 09:48:57 +01:00
5dc3e281ce and again 2023-01-30 09:45:28 +01:00
ce3afb821f another attempt changing the owner to fix permission issue 2023-01-30 09:43:54 +01:00
2652a351ad remove chown again 2023-01-30 09:38:57 +01:00
9109722409 fix main.yml 2023-01-30 09:37:12 +01:00
f861c262a1 update ci actions, hopefully fix permission error 2023-01-30 09:15:09 +01:00
bbfb5eba49 add back event debouncing in file watcher 2023-01-30 00:09:25 +01:00
9e19ff1761 prepare for 0.2.2 release 2023-01-28 12:47:16 +01:00
5d85aeac09 call an exported start function if it exists 2023-01-28 12:31:16 +01:00
30eb953d5d remove test code that was accidentally committed 2023-01-28 11:37:05 +01:00
abdf780533 only open browser once cart has been compiled succesfully 2023-01-28 11:33:49 +01:00
fc05a54e2c fix control codes 4-6 as parameters to other control codes 2023-01-28 10:58:28 +01:00
b33099c828 update more dependencies 2023-01-28 02:11:25 +01:00
d478d3ad49 enable timeapi in winapi crate 2023-01-27 00:13:32 +01:00
502852e59a update uw8-window dependencies 2023-01-26 23:40:54 +01:00
5efa8b3465 first batch of dependency updates 2023-01-26 22:45:34 +01:00
daf2a02cd8 fix sndGes name & add missing auto-import 2023-01-23 23:21:45 +01:00
8d5374a867 add support to ignore empty memory section when packing 2022-11-03 23:27:53 +01:00
142b6a4c15 fix typo in sample rate selection 2022-10-30 00:07:58 +02:00
877fceb089 add --fps parameter to output fps 2022-07-25 23:44:06 +02:00
f0ba0f2b99 update site with 0.2.1 release 2022-07-25 08:46:23 +02:00
760664eb77 add some command line switches for the gpu window 2022-07-23 22:34:59 +02:00
465e66ff4b slight improvement to packed size display 2022-07-23 00:30:14 +02:00
e4579d81bc add chromatic version of fast crt shader + auto crt shader 2022-07-21 23:03:52 +02:00
1f5042059c fix inputs getting stuck 2022-07-21 19:37:36 +02:00
499bb02f2c restructure control flow of uw8-window to hopefully make it work on MacOS 2022-07-21 08:51:17 +02:00
57a92ba79a update version number 2022-07-17 22:44:46 +02:00
7ec1e68a00 very slightly improve frame timings when not quite reaching 60 fps 2022-07-14 00:05:15 +02:00
539d19e0d7 fix square filter, arrow keys on windows 2022-07-12 20:18:06 +02:00
e9a5f702b4 finish fast_crt shader 2022-07-12 00:22:05 +02:00
ba0b037ec2 add first version of fast crt shader 2022-07-11 09:26:11 +02:00
0130d1c906 implement square filter 2022-07-10 23:56:19 +02:00
379ece5cbf restructuring for multiple filters 2022-07-10 23:17:31 +02:00
c9c5cb76bd start refactoring wgpu code to allow for different upscale filters 2022-07-10 16:37:39 +02:00
a6d6615231 only draw area covered by framebuffer 2022-07-10 12:18:53 +02:00
fbc86fa78d implement input for gpu window 2022-07-09 21:16:25 +02:00
eb724e8785 keyboard input is working for cpu window again 2022-07-09 13:18:51 +02:00
f559c5b7d4 restructure run_native to report errors back to caller 2022-07-09 12:24:59 +02:00
57 changed files with 4521 additions and 2167 deletions

View File

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

View File

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

2522
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "uw8"
version = "0.2.0"
version = "0.2.2"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -11,19 +11,21 @@ native = ["wasmtime", "uw8-window", "cpal", "rubato" ]
browser = ["warp", "tokio", "tokio-stream", "webbrowser"]
[dependencies]
wasmtime = { version = "0.37.0", optional = true }
wasmtime = { version = "5.0.0", optional = true }
anyhow = "1"
env_logger = "0.10"
log = "0.4"
uw8-window = { path = "uw8-window", optional = true }
notify = "4"
pico-args = "0.4"
notify-debouncer-mini = { version = "0.2.1", default-features = false }
pico-args = "0.5"
curlywas = { git = "https://github.com/exoticorn/curlywas.git", rev = "0e7ea50" }
wat = "1"
uw8-tool = { path = "uw8-tool" }
same-file = "1"
warp = { version = "0.3.2", optional = true }
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 }
warp = { version = "0.3.3", optional = true }
tokio = { version = "1.24.0", features = ["sync", "rt"], optional = true }
tokio-stream = { version = "0.1.11", features = ["sync"], optional = true }
webbrowser = { version = "0.8.6", 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.2", optional = true }
rubato = { version = "0.12.0", optional = true }

View File

@@ -15,9 +15,9 @@ See [here](https://exoticorn.github.io/microw8/) for more information and docs.
## Downloads
* [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)
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.2.2/microw8-0.2.2-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.2.2/microw8-0.2.2-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.2.2/microw8-0.2.2-windows.zip)
The download includes
@@ -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,21 @@
include "../include/microw8-api.cwa"
const SPRITE = 0x20000;
export fn upd() {
cls(0);
let t = time();
let i: i32;
loop spriteLoop {
let inline x = sin(t * -1.3 + i as f32 / 8_f) * 180_f + 160_f;
let inline y = sin(t * 1.7 + i as f32 / 9_f) * 140_f + 120_f;
blitSprite(SPRITE, 16, x as i32, y as i32, 0x100);
branch_if (i +:= 1) < 200: spriteLoop;
}
}
start fn start() {
printChar('OO');
circle(8_f, 8_f, 6_f, 75);
grabSprite(SPRITE, 16, 0, 0, 0);
}

View File

@@ -30,10 +30,13 @@ import "env.printInt" fn printInt(i32);
import "env.setTextColor" fn setTextColor(i32);
import "env.setBackgroundColor" fn setBackgroundColor(i32);
import "env.setCursorPosition" fn setCursorPosition(i32, i32);
import "env.rectangle_outline" fn rectangle_outline(f32, f32, f32, f32, i32);
import "env.circle_outline" fn circle_outline(f32, f32, f32, i32);
import "env.rectangleOutline" fn rectangleOutline(f32, f32, f32, f32, i32);
import "env.circleOutline" fn circleOutline(f32, f32, f32, i32);
import "env.exp" fn exp(f32) -> f32;
import "env.playNote" fn playNote(i32, i32);
import "env.sndGes" fn sndGes(i32) -> f32;
import "env.blitSprite" fn blitSprite(i32, i32, i32, i32, i32);
import "env.grabSprite" fn grabSprite(i32, i32, i32, i32, i32);
const TIME_MS = 0x40;
const GAMEPAD = 0x44;

View File

@@ -30,10 +30,13 @@
(import "env" "setTextColor" (func $setTextColor (param i32)))
(import "env" "setBackgroundColor" (func $setBackgroundColor (param i32)))
(import "env" "setCursorPosition" (func $setCursorPosition (param i32) (param i32)))
(import "env" "rectangle_outline" (func $rectangle_outline (param f32) (param f32) (param f32) (param f32) (param i32)))
(import "env" "circle_outline" (func $circle_outline (param f32) (param f32) (param f32) (param i32)))
(import "env" "rectangleOutline" (func $rectangleOutline (param f32) (param f32) (param f32) (param f32) (param i32)))
(import "env" "circleOutline" (func $circleOutline (param f32) (param f32) (param f32) (param i32)))
(import "env" "exp" (func $exp (param f32) (result f32)))
(import "env" "playNote" (func $playNote (param i32) (param i32)))
(import "env" "sndGes" (func $sndGes (param i32) (result f32)))
(import "env" "blitSprite" (func $blitSprite (param i32) (param i32) (param i32) (param i32) (param i32)))
(import "env" "grabSprite" (func $grabSprite (param i32) (param i32) (param i32) (param i32) (param i32)))
;; to use defines, include this file with a preprocessor
;; like gpp (https://logological.org/gpp).

View File

@@ -1,2 +1,2 @@
/zig-cache/
/zig-out/
zig-cache/
zig-out/

View File

@@ -5,11 +5,7 @@ pub fn build(b: *std.build.Builder) void {
const lib = b.addSharedLibrary("cart", "main.zig", .unversioned);
lib.setBuildMode(mode);
lib.setTarget(.{
.cpu_arch = .wasm32,
.os_tag = .freestanding,
.cpu_features_add = std.Target.wasm.featureSet(&.{ .nontrapping_fptoint })
});
lib.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding, .cpu_features_add = std.Target.wasm.featureSet(&.{.nontrapping_fptoint}) });
lib.import_memory = true;
lib.initial_memory = 262144;
lib.max_memory = 262144;
@@ -18,19 +14,13 @@ pub fn build(b: *std.build.Builder) void {
lib.install();
if (lib.install_step) |install_step| {
const run_filter_exports = b.addSystemCommand(&[_][]const u8{
"uw8", "filter-exports", "zig-out/lib/cart.wasm", "zig-out/lib/cart-filtered.wasm"
});
const run_filter_exports = b.addSystemCommand(&[_][]const u8{ "uw8", "filter-exports", "zig-out/lib/cart.wasm", "zig-out/lib/cart-filtered.wasm" });
run_filter_exports.step.dependOn(&install_step.step);
const run_wasm_opt = b.addSystemCommand(&[_][]const u8{
"wasm-opt", "-Oz", "-o", "zig-out/cart.wasm", "zig-out/lib/cart-filtered.wasm"
});
const run_wasm_opt = b.addSystemCommand(&[_][]const u8{ "wasm-opt", "--enable-nontrapping-float-to-int", "-Oz", "-o", "zig-out/cart.wasm", "zig-out/lib/cart-filtered.wasm" });
run_wasm_opt.step.dependOn(&run_filter_exports.step);
const run_uw8_pack = b.addSystemCommand(&[_][]const u8{
"uw8", "pack", "-l", "9", "zig-out/cart.wasm", "zig-out/cart.uw8"
});
const run_uw8_pack = b.addSystemCommand(&[_][]const u8{ "uw8", "pack", "-l", "5", "zig-out/cart.wasm", "zig-out/cart.uw8" });
run_uw8_pack.step.dependOn(&run_wasm_opt.step);
const make_opt = b.step("make_opt", "make size optimized cart");

142
examples/zig/game/main.zig Normal file
View File

@@ -0,0 +1,142 @@
const uw8 = @import("uw8.zig");
var redBallSprite: [16 * 16]u8 = undefined;
var greenBallSprite: [16 * 16]u8 = undefined;
var blueBallSprite: [16 * 16]u8 = undefined;
var wallSprite: [24 * 24]u8 = undefined;
const SphereConfigStep = struct { size: u8, color: u8 };
// zig fmt: off
const redSphereConfig: [4]SphereConfigStep = .{
.{ .size = 0, .color = 0x3d },
.{ .size = 2, .color = 0x48 },
.{ .size = 6, .color = 0x65 },
.{ .size = 9, .color = 0x55 }
};
const greenSphereConfig: [4]SphereConfigStep = .{
.{ .size = 0, .color = 0x7d },
.{ .size = 2, .color = 0x88 },
.{ .size = 6, .color = 0x96 },
.{ .size = 9, .color = 0xa3 }
};
const blueSphereConfig: [4]SphereConfigStep = .{
.{ .size = 0, .color = 0x2e },
.{ .size = 2, .color = 0x19 },
.{ .size = 6, .color = 0x17 },
.{ .size = 9, .color = 0x24 }
};
const levelData: [14] *const [19]u8 = .{
"xxxxxxxxxxxxxxxxxxx",
"x x x x",
"x x xx x xx x x",
"x x xxx x xxx x x",
"x xx x x xx x",
"x xx xxxxx xx x",
"x x x x",
"x xx xx xxx xx xx x",
"x x xx xx x x",
"xx x x x x xx",
"x xx xxx xxx xx x",
"x xx x x xx x",
"x x x x x",
"xxxxxxxxxxxxxxxxxxx",
};
// zig fmt: on
export fn start() void {
blitSphere(&redBallSprite, &redSphereConfig);
blitSphere(&greenBallSprite, &greenSphereConfig);
blitSphere(&blueBallSprite, &blueSphereConfig);
createWallSprite();
}
fn blitSphere(sprite: [*]u8, config: []const SphereConfigStep) void {
for (config) |circle| {
uw8.circle(8, 8, 8, circle.color);
uw8.circle(5, 6, @intToFloat(f32, circle.size), 0);
uw8.grabSprite(sprite, 16, 0, 0, 0x100);
}
}
fn createWallSprite() void {
uw8.cls(0xe4);
var i: i32 = 0;
while (i < 50) : (i += 1) {
const x = uw8.randomf() * 16;
const y = uw8.randomf() * 16;
const radius = uw8.randomf() * 2 + 1;
const c = @intCast(u8, (uw8.random() & 3)) + 0x95;
var j: i32 = 0;
while (j < 9) : (j += 1) {
uw8.circle(x + @intToFloat(f32, @rem(j, 3) * 16), y + @intToFloat(f32, @divFloor(j, 3) * 16), radius, c);
}
}
uw8.grabSprite(&wallSprite, 16, 16, 16, 0);
}
export fn upd() void {
uw8.cls(0);
var y: usize = 0;
while (y < levelData.len) : (y += 1) {
var x: usize = 0;
while (x < levelData[y].len) : (x += 1) {
if (levelData[y][x] == 'x') {
uw8.blitSprite(&wallSprite, 16, 8 + @intCast(i32, x) * 16, @intCast(i32, y) * 16, 0);
}
}
}
updateEnemy(&enemies[0], &redBallSprite);
updateEnemy(&enemies[1], &greenBallSprite);
updateEnemy(&enemies[2], &blueBallSprite);
}
const EntityState = struct { x: i32, y: i32, dir: u2 };
var enemies: [3]EntityState = .{
.{ .x = 16, .y = 16, .dir = 1 },
.{ .x = 16 * 18, .y = 16, .dir = 3 },
.{ .x = 16, .y = 16 * 12, .dir = 1 },
};
fn updateEnemy(enemy: *EntityState, sprite: [*]u8) void {
switch (enemy.dir) {
0 => enemy.y -= 1,
1 => enemy.x += 1,
2 => enemy.y += 1,
3 => enemy.x -= 1,
}
if (((enemy.x | enemy.y) & 15) == 0) {
const tx = @intCast(usize, enemy.x) / 16;
const ty = @intCast(usize, enemy.y) / 16;
var dir = enemy.dir;
var count: u32 = 0;
if (enemy.dir != 2 and levelData[ty - 1][tx] == ' ') {
dir = 0;
count += 1;
}
if (enemy.dir != 3 and levelData[ty][tx + 1] == ' ') {
count += 1;
if (uw8.random() % count == 0) {
dir = 1;
}
}
if (enemy.dir != 0 and levelData[ty + 1][tx] == ' ') {
count += 1;
if (uw8.random() % count == 0) {
dir = 2;
}
}
if (enemy.dir != 1 and levelData[ty][tx - 1] == ' ') {
count += 1;
if (uw8.random() % count == 0) {
dir = 3;
}
}
enemy.dir = dir;
}
uw8.blitSprite(sprite, 16, 8 + enemy.x, enemy.y, 0x100);
}

10
examples/zig/game/uw8.zig Normal file
View File

@@ -0,0 +1,10 @@
pub extern fn random() u32;
pub extern fn randomf() f32;
pub extern fn time() f32;
pub extern fn cls(color: u8) void;
pub extern fn circle(x: f32, y: f32, radiu: f32, color: u8) void;
pub extern fn blitSprite(spriteData: [*]u8, size: u32, x: i32, y: i32, ctrl: u32) void;
pub extern fn grabSprite(spriteData: [*]u8, size: u32, x: i32, y: i32, ctrl: u32) void;
pub extern fn printString(str: [*:0]u8) void;
pub extern fn printInt(value: i32) void;
pub extern fn printChar(char: u32) void;

View File

@@ -0,0 +1,31 @@
const std = @import("std");
pub fn build(b: *std.build.Builder) void {
const mode = std.builtin.Mode.ReleaseSmall;
const lib = b.addSharedLibrary("cart", "main.zig", .unversioned);
lib.setBuildMode(mode);
lib.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding, .cpu_features_add = std.Target.wasm.featureSet(&.{.nontrapping_fptoint}) });
lib.import_memory = true;
lib.initial_memory = 262144;
lib.max_memory = 262144;
lib.global_base = 81920;
lib.stack_size = 8192;
lib.install();
if (lib.install_step) |install_step| {
const run_filter_exports = b.addSystemCommand(&[_][]const u8{ "uw8", "filter-exports", "zig-out/lib/cart.wasm", "zig-out/lib/cart-filtered.wasm" });
run_filter_exports.step.dependOn(&install_step.step);
const run_wasm_opt = b.addSystemCommand(&[_][]const u8{ "wasm-opt", "--enable-nontrapping-float-to-int", "-Oz", "-o", "zig-out/cart.wasm", "zig-out/lib/cart-filtered.wasm" });
run_wasm_opt.step.dependOn(&run_filter_exports.step);
const run_uw8_pack = b.addSystemCommand(&[_][]const u8{ "uw8", "pack", "-l", "9", "zig-out/cart.wasm", "zig-out/cart.uw8" });
run_uw8_pack.step.dependOn(&run_wasm_opt.step);
const make_opt = b.step("make_opt", "make size optimized cart");
make_opt.dependOn(&run_uw8_pack.step);
b.default_step = make_opt;
}
}

View File

@@ -1,11 +1,11 @@
extern fn atan2(x: f32, y: f32) f32;
extern fn time() f32;
pub const FRAMEBUFFER: *[320*240]u8 = @intToPtr(*[320*240]u8, 120);
pub const FRAMEBUFFER: *[320 * 240]u8 = @intToPtr(*[320 * 240]u8, 120);
export fn upd() void {
var i: u32 = 0;
while(true) {
while (true) {
var t = time() * 63.0;
var x = @intToFloat(f32, (@intCast(i32, i % 320) - 160));
var y = @intToFloat(f32, (@intCast(i32, i / 320) - 120));
@@ -13,8 +13,10 @@ export fn upd() void {
var u = atan2(x, y) * 512.0 / 3.141;
var c = @intCast(u8, (@floatToInt(i32, d + t * 2.0) ^ @floatToInt(i32, u + t)) & 255) >> 4;
FRAMEBUFFER[@as(usize, i)] = c;
FRAMEBUFFER[i] = c;
i += 1;
if(i >= 320*240) { break; }
if (i >= 320 * 240) {
break;
}
}
}
}

186
platform/Cargo.lock generated
View File

@@ -110,9 +110,9 @@ dependencies = [
[[package]]
name = "crc32fast"
version = "1.3.0"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "738c290dfaea84fc1ca15ad9c168d083b05a714e1efddd8edaab678dc28d2836"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [
"cfg-if",
]
@@ -151,32 +151,39 @@ dependencies = [
"anyhow",
"ariadne",
"chumsky",
"pico-args",
"pico-args 0.4.2",
"wasm-encoder 0.10.0",
"wasmparser 0.83.0",
]
[[package]]
name = "fallible_collections"
version = "0.4.4"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52db5973b6a19247baf19b30f41c23a1bfffc2e9ce0a5db2f60e3cd5dc8895f7"
checksum = "3f57ccc32870366ae684be48b32a1a2e196f98a42a9b4361fe77e13fd4a34755"
dependencies = [
"hashbrown",
]
[[package]]
name = "flate2"
version = "1.0.22"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f"
checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
dependencies = [
"cfg-if",
"crc32fast",
"libc",
"miniz_oxide",
]
[[package]]
name = "form_urlencoded"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
dependencies = [
"percent-encoding",
]
[[package]]
name = "getrandom"
version = "0.2.3"
@@ -190,9 +197,9 @@ dependencies = [
[[package]]
name = "hashbrown"
version = "0.11.2"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
"ahash 0.7.6",
]
@@ -212,6 +219,26 @@ version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005"
[[package]]
name = "idna"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
dependencies = [
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "indexmap"
version = "1.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
@@ -225,17 +252,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
[[package]]
name = "libc"
version = "0.2.112"
name = "lexopt"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125"
checksum = "478ee9e62aaeaf5b140bd4138753d1f109765488581444218d3ddda43234f3e8"
[[package]]
name = "libc"
version = "0.2.139"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
[[package]]
name = "lodepng"
version = "3.4.7"
version = "3.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24844d5c0b922ddd52fb5bf0964a4c7f8e799a946ec01bb463771eb04fc1a323"
checksum = "f0ad39f75bbaa4b10bb6f2316543632a8046a5bcf9c785488d79720b21f044f8"
dependencies = [
"crc32fast",
"fallible_collections",
"flate2",
"libc",
@@ -253,12 +287,11 @@ dependencies = [
[[package]]
name = "miniz_oxide"
version = "0.4.4"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
dependencies = [
"adler",
"autocfg",
]
[[package]]
@@ -288,12 +321,24 @@ dependencies = [
"winapi",
]
[[package]]
name = "percent-encoding"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
name = "pico-args"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468"
[[package]]
name = "pico-args"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
[[package]]
name = "platform"
version = "0.1.0"
@@ -330,9 +375,9 @@ dependencies = [
[[package]]
name = "rgb"
version = "0.8.31"
version = "0.8.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a374af9a0e5fdcdd98c1c7b64f05004f9ea2555b6c75f211daa81268a3c50f1"
checksum = "3603b7d71ca82644f79b5a06d1220e9a58ede60bd32255f698cb1af8838b8db3"
dependencies = [
"bytemuck",
]
@@ -357,6 +402,26 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "thiserror"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "time"
version = "0.1.43"
@@ -376,6 +441,36 @@ dependencies = [
"crunchy",
]
[[package]]
name = "tinyvec"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "unicode-bidi"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58"
[[package]]
name = "unicode-normalization"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-segmentation"
version = "1.9.0"
@@ -390,13 +485,24 @@ checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "upkr"
version = "0.1.0"
source = "git+https://github.com/exoticorn/upkr.git?rev=2e7983fc#2e7983fc650788d98da2eecef2d16f63e849e4a0"
version = "0.2.1"
source = "git+https://github.com/exoticorn/upkr.git?rev=080db40d0088bbee2bdf3c5c75288ac7853d6b7a#080db40d0088bbee2bdf3c5c75288ac7853d6b7a"
dependencies = [
"anyhow",
"cdivsufsort",
"pbr",
"pico-args",
"lexopt",
"thiserror",
]
[[package]]
name = "url"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
]
[[package]]
@@ -405,11 +511,11 @@ version = "0.1.0"
dependencies = [
"anyhow",
"pbr",
"pico-args",
"pico-args 0.5.0",
"upkr",
"walrus",
"wasm-encoder 0.8.0",
"wasmparser 0.81.0",
"wasm-encoder 0.22.0",
"wasmparser 0.99.0",
]
[[package]]
@@ -452,18 +558,18 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]]
name = "wasm-encoder"
version = "0.8.0"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db0c351632e46cc06a58a696a6c11e4cf90cad4b9f8f07a0b59128d616c29bb0"
checksum = "aa9d9bf45fc46f71c407837c9b30b1e874197f2dc357588430b21e5017d290ab"
dependencies = [
"leb128",
]
[[package]]
name = "wasm-encoder"
version = "0.10.0"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9d9bf45fc46f71c407837c9b30b1e874197f2dc357588430b21e5017d290ab"
checksum = "ef126be0e14bdf355ac1a8b41afc89195289e5c7179f80118e3abddb472f0810"
dependencies = [
"leb128",
]
@@ -474,18 +580,22 @@ version = "0.77.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35c86d22e720a07d954ebbed772d01180501afe7d03d464f413bb5f8914a8d6"
[[package]]
name = "wasmparser"
version = "0.81.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98930446519f63d00a836efdc22f67766ceae8dbcc1571379f2bcabc6b2b9abc"
[[package]]
name = "wasmparser"
version = "0.83.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "718ed7c55c2add6548cca3ddd6383d738cd73b892df400e96b9aa876f0141d7a"
[[package]]
name = "wasmparser"
version = "0.99.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ef3b717afc67f848f412d4f02c127dd3e35a0eecd58c684580414df4fde01d3"
dependencies = [
"indexmap",
"url",
]
[[package]]
name = "winapi"
version = "0.3.9"

View File

@@ -9,4 +9,4 @@ edition = "2021"
curlywas = { git="https://github.com/exoticorn/curlywas.git", rev="0e7ea50" }
uw8-tool = { path="../uw8-tool" }
anyhow = "1"
lodepng = "3.4"
lodepng = "3.7.2"

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;

View File

@@ -171,7 +171,7 @@ export fn rectangle(x: f32, y: f32, w: f32, h: f32, col: i32) {
}
}
export fn rectangle_outline(x: f32, y: f32, w: f32, h: f32, col: i32) {
export fn rectangleOutline(x: f32, y: f32, w: f32, h: f32, col: i32) {
let xl = nearest(x) as i32;
let xr = nearest(x + w) as i32;
let yt = nearest(y) as i32;
@@ -212,7 +212,7 @@ export fn circle(cx: f32, cy: f32, radius: f32, col: i32) {
}
}
export fn circle_outline(cx: f32, cy: f32, radius: f32, col: i32) {
export fn circleOutline(cx: f32, cy: f32, radius: f32, col: i32) {
let prev_w: f32;
let y = clamp(nearest(cy - radius) as i32, -1, 241);
let maxY = clamp(nearest(cy + radius) as i32, -1, 241);
@@ -352,6 +352,92 @@ export fn line(x1: f32, y1: f32, x2: f32, y2: f32, col: i32) {
setPixel(i32.trunc_sat_f32_s(x1 + f * dx), i32.trunc_sat_f32_s(y1 + f * dy), col);
}
export fn blitSprite(sprite: i32, size: i32, x: i32, y: i32, control: i32) {
let lazy width = size & 65535;
let lazy height = select(size >> 16, size >> 16, width);
let lazy x0 = select(x < 0, -x, 0);
let lazy x1 = select(x + width > 320, 320 - x, width);
let lazy y0 = select(y < 0, -y, 0);
let lazy y1 = select(y + height > 240, 240 - y, height);
let lazy numRows = y1 - y0;
let lazy numCols = x1 - x0;
if numRows <= 0 | numCols <= 0 {
return;
}
let trans = (control & 511) - 256;
let lazy flip_x = 1 - ((control >> 8) & 2);
let lazy flip_y = 1 - ((control >> 9) & 2);
if flip_x < 0 {
sprite += width - 1;
}
if flip_y < 0 {
sprite += (height - 1) * width;
}
let spriteRow = sprite + x0 * flip_x + y0 * flip_y * width;
let screenRow = x + x0 + (y + y0) * 320;
loop yloop {
let lx = 0;
loop xloop {
let lazy col = (spriteRow + lx * flip_x)?0;
if col != trans {
(screenRow + lx)?120 = col;
}
branch_if (lx +:= 1) < numCols: xloop;
}
spriteRow += width * flip_y;
screenRow += 320;
branch_if numRows -:= 1: yloop;
}
}
export fn grabSprite(sprite: i32, size: i32, x: i32, y: i32, control: i32) {
let lazy width = size & 65535;
let lazy height = select(size >> 16, size >> 16, width);
let lazy x0 = select(x < 0, -x, 0);
let lazy x1 = select(x + width > 320, 320 - x, width);
let lazy y0 = select(y < 0, -y, 0);
let lazy y1 = select(y + height > 240, 240 - y, height);
let lazy numRows = y1 - y0;
let lazy numCols = x1 - x0;
if numRows <= 0 | numCols <= 0 {
return;
}
let trans = (control & 511) - 256;
let lazy flip_x = 1 - ((control >> 8) & 2);
let lazy flip_y = 1 - ((control >> 9) & 2);
if flip_x < 0 {
sprite += width - 1;
}
if flip_y < 0 {
sprite += (height - 1) * width;
}
let spriteRow = sprite + x0 * flip_x + y0 * flip_y * width;
let screenRow = x + x0 + (y + y0) * 320;
loop yloop {
let lx = 0;
loop xloop {
let lazy col = (screenRow + lx)?120;
if col != trans {
(spriteRow + lx * flip_x)?0 = col;
}
branch_if (lx +:= 1) < numCols: xloop;
}
spriteRow += width * flip_y;
screenRow += 320;
branch_if numRows -:= 1: yloop;
}
}
//////////
// TEXT //
//////////
@@ -372,16 +458,7 @@ 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 {
if outputChannel >= 2 & (char < 4 | char > 6) {
logChar(char);
return;
}
@@ -399,6 +476,15 @@ fn printSingleChar(char: i32) {
return;
}
if char >= 4 & char <= 6 {
outputChannel = char - 4;
if !outputChannel {
textCursorX = 0;
textCursorY = 0;
}
return;
}
if char == 7 {
80?0 = 80?0 ^ 2;
return;

View File

@@ -14,6 +14,8 @@ The initial motivation behind MicroW8 was to explore whether there was a way to
* Memory: 256KB
* Gamepad input (D-Pad + 4 Buttons)
For detailed [documentation see here](docs).
## Examples
* [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.
@@ -29,6 +31,33 @@ Examplers for older versions:
## Versions
### v0.2.2
* [Web runtime](v0.2.2)
* [Linux](https://github.com/exoticorn/microw8/releases/download/v0.2.2/microw8-0.2.2-linux.tgz)
* [MacOS](https://github.com/exoticorn/microw8/releases/download/v0.2.2/microw8-0.2.2-macos.tgz)
* [Windows](https://github.com/exoticorn/microw8/releases/download/v0.2.2/microw8-0.2.2-windows.zip)
Changes:
* call `start` function after loading cart if the cart exports one
* fix `sndGes` having the wrong name and not being included in the auto imports
* fix control codes 4-6 (change text output mode) being invoked when used as parameters in other control sequences
* only open browser window once a cart was compiled sucessfully when running with `-b`
### v0.2.1
* [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:
* 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)

View File

@@ -12,6 +12,9 @@ at offset 120 in memory with the 32bpp palette located at 0x13000.
The memory has to be imported as `env` `memory` and has a maximum size of 256kb (4 pages).
If the module exports a function called `start`, it will be called once after the module is
loaded.
# Memory map
```
@@ -143,13 +146,13 @@ Fills the circle at `cx, cy` and with `radius` with the given color index.
(Sets all pixels where the pixel center lies inside the circle.)
### fn rectangle_outline(x: f32, y: f32, w: f32, h: f32, color: i32)
### fn rectangleOutline(x: f32, y: f32, w: f32, h: f32, color: i32)
Draws a one pixel outline on the inside of the given rectangle.
(Draws the outermost pixels that are still inside the rectangle area.)
### fn circle_outline(cx: f32, cy: f32, radius: f32, color: i32)
### fn circleOutline(cx: f32, cy: f32, radius: f32, color: i32)
Draws a one pixel outline on the inside of the given circle.
@@ -159,6 +162,21 @@ Draws a one pixel outline on the inside of the given circle.
Draws a line from `x1,y1` to `x2,y2` in the given color index.
### fn blitSprite(spriteData: i32, size: i32, x: i32, y: i32, control: i32)
Copies the pixel data at `spriteData` onto the screen at `x`, `y`. The size of the sprite is passed as `width | (height << 16)`.
If the height is given as 0, the sprite is is treated as square (width x width).
The control parameter controls masking and flipping of the sprite:
* bits 0-7: color mask index
* bit 8: switch on masked blit (pixel with color mask index are treated as transparent)
* bit 9: flip sprite x
* bit 10: flip sprite y
### fn grabSprite(spriteData: i32, size: i32, x: i32, y: i32, control: i32)
Copies the pixel data on the screen at `x`, `y` to `spriteData`. Parameters are exactly the same as `blitSprite`.
## Input
MicroW8 provides input from a gamepad with one D-Pad and 4 buttons, or a keyboard emulation thereof.
@@ -436,6 +454,30 @@ and execution of the cart is stopped. Defaults to 30 (0.5s)
* `-l LEVEL`, `--level LEVEL`: Compression level (0-9). Higher compression levels are really slow.
* `-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:

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.2.0">
<a href="v0.2.2">
<img class="demonstration-gif" style="width:640px;height:480px;image-rendering:pixelated" src="img/technotunnel.png"></img>
</a>
</div>

View File

@@ -1,9 +1,13 @@
use anyhow::{anyhow, bail, Result};
use notify::{DebouncedEvent, RecommendedWatcher, Watcher};
use notify_debouncer_mini::{
new_debouncer,
notify::{self, RecommendedWatcher},
DebouncedEvent, DebouncedEventKind, Debouncer,
};
use std::{collections::BTreeSet, path::PathBuf, sync::mpsc, time::Duration};
pub struct FileWatcher {
watcher: RecommendedWatcher,
debouncer: Debouncer<RecommendedWatcher>,
watched_files: BTreeSet<PathBuf>,
directories: BTreeSet<PathBuf>,
rx: mpsc::Receiver<DebouncedEvent>,
@@ -12,9 +16,20 @@ pub struct FileWatcher {
impl FileWatcher {
pub fn new() -> Result<FileWatcher> {
let (tx, rx) = mpsc::channel();
let watcher = notify::watcher(tx, Duration::from_millis(100))?;
let debouncer = new_debouncer(Duration::from_millis(100), None, move |res| match res {
Ok(events) => {
for event in events {
let _ = tx.send(event);
}
}
Err(errs) => {
for err in errs {
eprintln!("Error watching for file changes: {err}");
}
}
})?;
Ok(FileWatcher {
watcher,
debouncer,
watched_files: BTreeSet::new(),
directories: BTreeSet::new(),
rx,
@@ -26,7 +41,8 @@ impl FileWatcher {
let parent = path.parent().ok_or_else(|| anyhow!("File has no parent"))?;
if !self.directories.contains(parent) {
self.watcher
self.debouncer
.watcher()
.watch(parent, notify::RecursiveMode::NonRecursive)?;
self.directories.insert(parent.to_path_buf());
}
@@ -36,16 +52,18 @@ impl FileWatcher {
}
pub fn poll_changed_file(&self) -> Result<Option<PathBuf>> {
let event = self.rx.try_recv();
match event {
Ok(DebouncedEvent::Create(path) | DebouncedEvent::Write(path)) => {
let handle = same_file::Handle::from_path(&path)?;
for file in &self.watched_files {
if handle == same_file::Handle::from_path(file)? {
return Ok(Some(path));
match self.rx.try_recv() {
Ok(event) => match event.kind {
DebouncedEventKind::Any => {
let handle = same_file::Handle::from_path(&event.path)?;
for file in &self.watched_files {
if handle == same_file::Handle::from_path(file)? {
return Ok(Some(event.path));
}
}
}
}
_ => (),
},
Err(mpsc::TryRecvError::Disconnected) => bail!("File watcher disconnected"),
_ => (),
}

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
@@ -80,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()))?;
@@ -93,7 +104,7 @@ fn run(mut args: Arguments) -> Result<()> {
unimplemented!();
#[cfg(feature = "native")]
{
let mut microw8 = MicroW8::new(timeout)?;
let mut microw8 = MicroW8::new(timeout, window_config)?;
if disable_audio {
microw8.disable_audio();
}
@@ -165,7 +176,8 @@ 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!(
"\npacked size: {:.2} bytes",
"\npacked size: {} bytes ({:.2})",
cart.len(),
uw8_tool::compressed_size(&cart)
);
}

File diff suppressed because one or more lines are too long

View File

@@ -5,12 +5,20 @@ use std::{thread, time::Instant};
use anyhow::{anyhow, Result};
use cpal::traits::*;
use rubato::Resampler;
use uw8_window::{Window, WindowConfig};
use wasmtime::{
Engine, GlobalType, Memory, MemoryType, Module, Mutability, Store, TypedFunc, ValType,
};
pub struct MicroW8 {
tx: mpsc::SyncSender<Vec<u8>>,
window: Window,
stream: Option<cpal::Stream>,
engine: Engine,
loader_module: Module,
disable_audio: bool,
module_data: Option<Vec<u8>>,
timeout: u32,
instance: Option<UW8Instance>,
}
struct UW8Instance {
@@ -19,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 {
@@ -38,7 +45,7 @@ struct UW8WatchDog {
}
impl MicroW8 {
pub fn new(timeout: Option<u32>) -> Result<MicroW8> {
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() {
@@ -49,64 +56,32 @@ impl MicroW8 {
let loader_module =
wasmtime::Module::new(&engine, include_bytes!("../platform/bin/loader.wasm"))?;
let (tx, rx) = mpsc::sync_channel::<Vec<u8>>(1);
let window = Window::new(window_config)?;
std::thread::spawn(move || {
let mut state = State {
engine,
loader_module,
disable_audio: false,
instance: None,
timeout: timeout.unwrap_or(0),
};
uw8_window::run(move |framebuffer, gamepad, reset| {
if let Ok(module_data) = rx.try_recv() {
if let Err(err) = state.load(&module_data) {
eprintln!("Failed to load module: {}", err);
}
}
state
.run_frame(framebuffer, gamepad, reset)
.unwrap_or_else(|err| {
eprintln!("Runtime error: {}", err);
Instant::now()
})
});
});
Ok(MicroW8 { tx })
Ok(MicroW8 {
window,
stream: None,
engine,
loader_module,
disable_audio: false,
module_data: None,
timeout: timeout.unwrap_or(0),
instance: None,
})
}
pub fn disable_audio(&mut self) {}
pub fn disable_audio(&mut self) {
self.disable_audio = true;
}
}
impl super::Runtime for MicroW8 {
fn is_open(&self) -> bool {
true
self.window.is_open()
}
fn load(&mut self, module_data: &[u8]) -> Result<()> {
self.tx.send(module_data.into())?;
Ok(())
}
fn run_frame(&mut self) -> Result<()> {
Ok(())
}
}
struct State {
engine: Engine,
loader_module: Module,
disable_audio: bool,
instance: Option<UW8Instance>,
timeout: u32,
}
impl State {
fn load(&mut self, module_data: &[u8]) -> Result<()> {
self.stream = None;
self.instance = None;
let mut store = wasmtime::Store::new(&self.engine, ());
@@ -118,7 +93,7 @@ impl State {
linker.define("env", "memory", memory)?;
let loader_instance = linker.instantiate(&mut store, &self.loader_module)?;
let load_uw8 = loader_instance.get_typed_func::<i32, i32, _>(&mut store, "load_uw8")?;
let load_uw8 = loader_instance.get_typed_func::<i32, i32>(&mut store, "load_uw8")?;
let platform_data = include_bytes!("../platform/bin/platform.uw8");
memory.data_mut(&mut store)[..platform_data.len()].copy_from_slice(platform_data);
@@ -156,20 +131,24 @@ impl State {
}
let instance = linker.instantiate(&mut store, &module)?;
let end_frame = platform_instance.get_typed_func::<(), (), _>(&mut store, "endFrame")?;
let update = instance.get_typed_func::<(), (), _>(&mut store, "upd").ok();
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
if let Some(start) = instance.get_typed_func::<(), ()>(&mut store, "start").ok() {
start.call(&mut store, ())?;
}
let (sound_tx, stream) = if self.disable_audio {
(None, None)
} else {
match init_sound(&self.engine, &platform_module, &module) {
Ok(sound) => {
sound.stream.play()?;
Some(sound)
(Some(sound.tx), Some(sound.stream))
}
Err(err) => {
eprintln!("Failed to init sound: {}", err);
None
(None, None)
}
}
};
@@ -180,32 +159,40 @@ impl State {
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,
framebuffer: &mut dyn uw8_window::Framebuffer,
gamepad: u32,
reset: bool,
) -> Result<Instant> {
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(now);
let mut result = Ok(());
if let Some(mut instance) = self.instance.take() {
let time = (now - instance.start_time).as_millis() as i32;
{
let offset = ((time as u32 as i64 * 6) % 100 - 50) / 6;
result = Ok(now + Duration::from_millis((16 - offset) as u64));
}
let next_frame = {
let frame = (time as u32 as u64 * 6 / 100) as u32;
let cur_offset = (time as u32).wrapping_sub((frame as u64 * 100 / 6) as u32);
let next_time =
((frame as u64 + 1) * 100 / 6 + cur_offset.max(1).min(4) as u64) as u32;
let offset = next_time.wrapping_sub(time as u32);
now + Duration::from_millis(offset as u64)
};
{
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);
}
instance.store.set_epoch_deadline(self.timeout as u64);
@@ -220,25 +207,25 @@ impl State {
let mut sound_regs = [0u8; 32];
sound_regs.copy_from_slice(&memory[80..112]);
if let Some(ref sound) = instance.sound {
sound.tx.send(RegisterUpdate {
if let Some(ref sound_tx) = instance.sound_tx {
let _ = sound_tx.send(RegisterUpdate {
time,
data: sound_regs,
})?;
});
}
let framebuffer_mem = &memory[120..(120 + 320 * 240)];
let palette_mem = &memory[0x13000..];
framebuffer.update(framebuffer_mem, palette_mem);
self.window
.end_frame(framebuffer_mem, palette_mem, next_frame);
if reset {
self.load(&instance.module)?;
} else if result.is_ok() {
if result.is_ok() {
self.instance = Some(instance);
}
}
Ok(result?)
result?;
Ok(())
}
}
@@ -332,8 +319,8 @@ fn init_sound(
let instance = linker.instantiate(&mut store, module)?;
let snd = instance
.get_typed_func::<(i32,), f32, _>(&mut store, "snd")
.or_else(|_| platform_instance.get_typed_func::<(i32,), f32, _>(&mut store, "gesSnd"))?;
.get_typed_func::<(i32,), f32>(&mut store, "snd")
.or_else(|_| platform_instance.get_typed_func::<(i32,), f32>(&mut store, "sndGes"))?;
let host = cpal::default_host();
let device = host
@@ -361,7 +348,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,

View File

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

View File

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

BIN
test.wasm

Binary file not shown.

View File

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

5
test/start_fn.cwa Normal file
View File

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

13
test/text_modes.cwa Normal file
View File

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

136
uw8-tool/Cargo.lock generated
View File

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

View File

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

View File

@@ -3,7 +3,7 @@ use std::{collections::HashMap, fs::File, path::Path};
use anyhow::{bail, Result};
use std::io::prelude::*;
use wasm_encoder::{
CodeSection, EntityType, Export, ExportSection, Function, FunctionSection, ImportSection,
CodeSection, EntityType, ExportKind, ExportSection, Function, FunctionSection, ImportSection,
Instruction, MemoryType, Module, TypeSection, ValType,
};
use ValType::*;
@@ -152,14 +152,14 @@ impl BaseModule {
add_function(
&mut functions,
&type_map,
"rectangle_outline",
"rectangleOutline",
&[F32, F32, F32, F32, I32],
None,
);
add_function(
&mut functions,
&type_map,
"circle_outline",
"circleOutline",
&[F32, F32, F32, I32],
None,
);
@@ -167,6 +167,22 @@ 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));
add_function(
&mut functions,
&type_map,
"blitSprite",
&[I32, I32, I32, I32, I32],
None,
);
add_function(
&mut functions,
&type_map,
"grabSprite",
&[I32, I32, I32, I32, I32],
None,
);
for i in functions.len()..64 {
add_function(
@@ -217,13 +233,13 @@ impl BaseModule {
let mut imports = ImportSection::new();
for (module, name, type_) in &self.function_imports {
imports.import(*module, Some(name.as_str()), EntityType::Function(*type_));
imports.import(*module, name.as_str(), EntityType::Function(*type_));
}
for (module, name, import) in &self.global_imports {
imports.import(
*module,
Some(name.as_str()),
name.as_str(),
EntityType::Global(wasm_encoder::GlobalType {
val_type: import.type_,
mutable: import.mutable,
@@ -233,11 +249,12 @@ impl BaseModule {
imports.import(
"env",
Some("memory"),
"memory",
MemoryType {
minimum: self.memory as u64,
maximum: None,
memory64: false,
shared: false,
},
);
@@ -258,7 +275,7 @@ impl BaseModule {
let mut exports = ExportSection::new();
for (name, fnc) in &self.exports {
exports.export(*name, Export::Function(*fnc));
exports.export(*name, ExportKind::Func, *fnc);
}
module.section(&exports);
@@ -286,7 +303,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, false, None);
let data = upkr::pack(&base1, 4, &upkr::Config::default(), None);
File::create(path)?.write_all(&data)?;
Ok(())
}

View File

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

View File

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

847
uw8-window/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,12 +6,13 @@ 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"
winit = "0.27.5"
env_logger = "0.10"
log = "0.4"
wgpu = "0.13.1"
pollster = "0.2"
bytemuck = { version = "1.4", features = [ "derive" ] }
pico-args = "0.5"
wgpu = "0.15"
pollster = "0.2.5"
bytemuck = { version = "1.13", features = [ "derive" ] }
anyhow = "1"
minifb = { version = "0.23.0", default-features = false, features = ["x11"] }
winapi = "0.3.9"
winapi = { version = "0.3.9", features = [ "timeapi" ] }

View File

@@ -1,7 +1,8 @@
use std::time::Instant;
use crate::Framebuffer;
use minifb::{Key, Window, WindowOptions};
use crate::{Input, WindowImpl};
use anyhow::Result;
use minifb::{Key, WindowOptions};
static GAMEPAD_KEYS: &[Key] = &[
Key::Up,
@@ -14,45 +15,53 @@ static GAMEPAD_KEYS: &[Key] = &[
Key::S,
];
pub fn run(mut update: Box<dyn FnMut(&mut dyn Framebuffer, u32, bool) -> Instant + 'static>) -> ! {
#[cfg(target_os = "windows")]
unsafe {
winapi::um::timeapi::timeBeginPeriod(1);
}
pub struct Window {
window: minifb::Window,
buffer: Vec<u32>,
}
let mut buffer: Vec<u32> = vec![0; 320 * 240];
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).unwrap();
let mut next_frame = Instant::now();
while window.is_open() && !window.is_key_down(Key::Escape) {
if let Some(sleep) = next_frame.checked_duration_since(Instant::now()) {
std::thread::sleep(sleep);
impl Window {
pub fn new() -> Result<Window> {
#[cfg(target_os = "windows")]
unsafe {
winapi::um::timeapi::timeBeginPeriod(1);
}
next_frame = update(
&mut CpuFramebuffer {
buffer: &mut buffer,
},
0,
false,
);
window.update_with_buffer(&buffer, 320, 240).unwrap();
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 })
}
std::process::exit(0);
}
struct CpuFramebuffer<'a> {
buffer: &'a mut Vec<u32>,
}
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;
}
}
impl<'a> Framebuffer for CpuFramebuffer<'a> {
fn update(&mut self, framebuffer: &[u8], palette: &[u8]) {
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
@@ -60,5 +69,15 @@ impl<'a> Framebuffer for CpuFramebuffer<'a> {
| ((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)
}
}

View File

@@ -1,469 +0,0 @@
use crate::Framebuffer;
use anyhow::{anyhow, Result};
use std::{num::NonZeroU32, time::Instant};
use wgpu::util::DeviceExt;
use winit::{
dpi::PhysicalSize,
event::{Event, VirtualKeyCode, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::{Fullscreen, WindowBuilder},
};
#[cfg(unix)]
use winit::platform::unix::EventLoopExtUnix;
pub struct Window {
event_loop: EventLoop<()>,
window: winit::window::Window,
instance: wgpu::Instance,
surface: wgpu::Surface,
adapter: wgpu::Adapter,
device: wgpu::Device,
queue: wgpu::Queue,
}
impl Window {
pub fn new() -> Result<Window> {
async fn create() -> Result<Window> {
let event_loop = EventLoop::new_any_thread();
let window = WindowBuilder::new()
.with_inner_size(PhysicalSize::new(640u32, 480))
.with_min_inner_size(PhysicalSize::new(320u32, 240))
.with_title("MicroW8")
.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?;
Ok(Window {
event_loop,
window,
instance,
surface,
adapter,
device,
queue,
})
}
pollster::block_on(create())
}
pub fn run(
self,
mut update: Box<dyn FnMut(&mut dyn Framebuffer, u32, bool) -> Instant + 'static>,
) -> ! {
let Window {
event_loop,
window,
instance,
surface,
adapter,
device,
queue,
} = self;
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 mut uniforms = Uniforms {
texture_scale: texture_scale_from_resolution(window.inner_size()),
};
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 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 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_texture_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: uniform_buffer.as_entire_binding(),
},
],
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,
});
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 mut 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 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_config.format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: Default::default(),
depth_stencil: None,
multisample: Default::default(),
multiview: None,
});
surface.configure(&device, &surface_config);
event_loop.run(move |event, _, control_flow| {
let _ = (&window, &instance, &surface, &adapter, &device);
match event {
Event::WindowEvent { event, .. } => match event {
WindowEvent::Resized(new_size) => {
surface_config.width = new_size.width;
surface_config.height = new_size.height;
surface.configure(&device, &surface_config);
uniforms.texture_scale = texture_scale_from_resolution(new_size);
queue.write_buffer(&uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
}
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
WindowEvent::KeyboardInput { input, .. } => {
if input.state == winit::event::ElementState::Pressed {
match input.virtual_keycode {
Some(VirtualKeyCode::Escape) => *control_flow = ControlFlow::Exit,
Some(VirtualKeyCode::F) => {
window.set_fullscreen(if window.fullscreen().is_some() {
None
} else {
Some(Fullscreen::Borderless(None))
});
}
_ => (),
}
}
}
_ => (),
},
Event::MainEventsCleared => {
if let ControlFlow::WaitUntil(t) = *control_flow {
if Instant::now() < t {
return;
}
}
let next_frame = update(
&mut GpuFramebuffer {
queue: &queue,
framebuffer: &framebuffer_texture,
palette: &palette_texture,
},
0,
false,
);
*control_flow = ControlFlow::WaitUntil(next_frame);
let output = surface.get_current_texture().unwrap();
let view = output
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
{
let mut render_pass =
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: None,
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &screen_texture_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: true,
},
})],
depth_stencil_attachment: None,
});
render_pass.set_pipeline(&palette_pipeline);
render_pass.set_bind_group(0, &palette_bind_group, &[]);
render_pass.draw(0..3, 0..1);
}
{
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::Load,
store: true,
},
})],
depth_stencil_attachment: None,
});
render_pass.set_pipeline(&render_pipeline);
render_pass.set_bind_group(0, &crt_bind_group, &[]);
render_pass.draw(0..3, 0..1);
}
queue.submit(std::iter::once(encoder.finish()));
output.present();
}
_ => (),
}
});
}
}
struct GpuFramebuffer<'a> {
framebuffer: &'a wgpu::Texture,
palette: &'a wgpu::Texture,
queue: &'a wgpu::Queue,
}
impl<'a> Framebuffer for GpuFramebuffer<'a> {
fn update(&mut self, pixels: &[u8], palette: &[u8]) {
self.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,
},
);
self.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 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],
}

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

@@ -14,10 +14,11 @@ fn vs_main(
@builtin(vertex_index) in_vertex_index: u32,
) -> VertexOutput {
var out: VertexOutput;
let x = (1.0 - f32(in_vertex_index)) * 3.0;
let y = f32(in_vertex_index & 1u) * 3.0 - 1.0;
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
out.tex_coords = vec2<f32>(x, y) * uniforms.texture_scale.xy + vec2<f32>(160.0, 120.0);
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;
}
@@ -28,47 +29,43 @@ fn sample_pixel(coords: vec2<i32>, offset: vec4<f32>) -> vec3<f32> {
if(is_outside) {
return vec3<f32>(0.0);
} else {
let f = max(vec4<f32>(0.01) / offset - vec4<f32>(0.003), vec4<f32>(0.0));
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);
if(pixel.x < -1 || pixel.y < -1 || pixel.x > 320 || pixel.y > 240) {
return vec4<f32>(0.0, 0.0, 0.0, 1.0);
}
let pixelf = floor(in.tex_coords);
let o = vec2<f32>(0.5) - (in.tex_coords - pixelf);
let pixel = vec2<i32>(pixelf);
let offset_x = o.xxxx + vec4<f32>(-0.125, 0.375, 0.125, -0.375) * uniforms.texture_scale.z;
let offset_y = o.yyyy + vec4<f32>(-0.375, -0.125, 0.375, 0.125) * uniforms.texture_scale.z;
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));
var offset_x0 = max(abs(offset_x + vec4<f32>(-1.0)) - vec4<f32>(0.5), vec4<f32>(0.0));
var offset_x1 = max(abs(offset_x) - vec4<f32>(0.5), vec4<f32>(0.0));
var offset_x2 = max(abs(offset_x + vec4<f32>(1.0)) - vec4<f32>(0.5), vec4<f32>(0.0));
let offset_x0 = offset_x0 * offset_x0;
let offset_x1 = offset_x1 * offset_x1;
let offset_x2 = offset_x2 * offset_x2;
offset_x0 = offset_x0 * offset_x0;
offset_x1 = offset_x1 * offset_x1;
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 offset_yr = offset_y + vec4<f32>(-1.0);
offset_yr = vec4<f32>(0.02) + offset_yr * offset_yr;
var acc = sample_pixel(pixel + vec2<i32>(-1, -1), offset_x0 + offset_yr);
acc = acc + sample_pixel(pixel + vec2<i32>(0, -1), offset_x1 + offset_yr);
acc = acc + sample_pixel(pixel + vec2<i32>(1, -1), offset_x2 + offset_yr);
let offset_yr = vec4<f32>(0.02) + offset_y * offset_y;
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;
offset_yr = offset_y + vec4<f32>(1.0);
offset_yr = vec4<f32>(0.02) + offset_yr * offset_yr;
acc = acc + sample_pixel(pixel + vec2<i32>(-1, 1), offset_x0 + offset_yr);
acc = acc + sample_pixel(pixel + vec2<i32>(0, 1), offset_x1 + offset_yr);

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(Default::default());
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 {
present_mode: wgpu::PresentMode::AutoNoVsync,
..surface.get_default_config(&adapter, window.inner_size().width, window.inner_size().height).expect("Surface incompatible with adapter")
};
let filter: Box<dyn Filter> = create_filter(
&device,
&palette_screen_mode.screen_view,
window.inner_size(),
surface_config.format,
window_config.filter,
);
surface.configure(&device, &surface_config);
Ok(Window {
event_loop,
window,
_instance: instance,
surface,
_adapter: adapter,
device,
queue,
palette_screen_mode,
surface_config,
filter,
gamepads: [0; 4],
next_frame: Instant::now(),
is_fullscreen: window_config.fullscreen,
is_open: true,
})
}
pollster::block_on(create(window_config))
}
}
impl WindowImpl for Window {
fn begin_frame(&mut self) -> Input {
let mut reset = false;
self.event_loop.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,
view_formats: &[]
});
let palette_texture = device.create_texture(&wgpu::TextureDescriptor {
size: wgpu::Extent3d {
width: 256,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D1,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
label: None,
view_formats: &[]
});
let screen_texture = device.create_texture(&wgpu::TextureDescriptor {
size: wgpu::Extent3d {
width: 320,
height: 240,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
label: None,
view_formats: &[]
});
let framebuffer_texture_view =
framebuffer_texture.create_view(&wgpu::TextureViewDescriptor::default());
let palette_texture_view =
palette_texture.create_view(&wgpu::TextureViewDescriptor::default());
let screen_texture_view =
screen_texture.create_view(&wgpu::TextureViewDescriptor::default());
let palette_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Uint,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D1,
sample_type: wgpu::TextureSampleType::Float { filterable: false },
},
count: None,
},
],
label: None,
});
let palette_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &palette_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&framebuffer_texture_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&palette_texture_view),
},
],
label: None,
});
let palette_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(include_str!("palette.wgsl").into()),
});
let palette_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[&palette_bind_group_layout],
push_constant_ranges: &[],
});
let palette_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(&palette_pipeline_layout),
vertex: wgpu::VertexState {
module: &palette_shader,
entry_point: "vs_main",
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &palette_shader,
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba8UnormSrgb,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: Default::default(),
depth_stencil: None,
multisample: Default::default(),
multiview: None,
});
PaletteScreenMode {
framebuffer: framebuffer_texture,
palette: palette_texture,
screen_view: screen_texture_view,
bind_group: palette_bind_group,
pipeline: palette_pipeline,
}
}
fn write_framebuffer(&self, queue: &wgpu::Queue, pixels: &[u8]) {
queue.write_texture(
wgpu::ImageCopyTexture {
texture: &self.framebuffer,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&bytemuck::cast_slice(pixels),
wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: 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,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));
}

View File

@@ -1,19 +1,117 @@
use anyhow::Result;
use std::time::Instant;
mod cpu;
mod gpu;
pub fn run<F: 'static + FnMut(&mut dyn Framebuffer, u32, bool) -> Instant>(update: F) -> ! {
match gpu::Window::new() {
Ok(window) => window.run(Box::new(update)),
Err(err) => eprintln!(
"Failed to create gpu window: {}\nFalling back to cpu window",
err
),
}
cpu::run(Box::new(update));
pub struct Window {
inner: Box<dyn WindowImpl>,
fps_counter: Option<FpsCounter>,
}
pub trait Framebuffer {
fn update(&mut self, pixels: &[u8], palette: &[u8]);
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;
}

View File

@@ -1,10 +1,13 @@
use std::time::{Duration, Instant};
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 start_time = Instant::now();
let mut start_time = Instant::now();
let mut palette = vec![0u32; 256];
for i in 0..256 {
@@ -15,14 +18,30 @@ fn main() {
palette[i as usize] = r + (g << 8) + (b << 16);
}
let mut prev_frame = Instant::now();
let mut fps_start = Instant::now();
let mut fps_counter = 0;
uw8_window::run(move |gpu_framebuffer, _gamepads, _reset| {
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());
gpu_framebuffer.update(&framebuffer, bytemuck::cast_slice(&palette));
prev_frame += Duration::from_secs_f32(1.0 / 60.0);
prev_frame
});
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) {

View File

@@ -63,7 +63,7 @@ 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);
}

View File

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

View File

@@ -263,6 +263,10 @@ export default function MicroW8(screen, config = {}) {
window.addEventListener('blur', () => updateVisibility(false), { signal: abortController.signal });
updateVisibility(document.hasFocus());
if (instance.exports.start) {
instance.exports.start();
}
function mainloop() {
if (!keepRunning) {
return;
@@ -270,7 +274,7 @@ export default function MicroW8(screen, config = {}) {
try {
let restart = false;
let thisFrame;
let nextFrame = 0;
if (!isPaused) {
let gamepads = navigator.getGamepads();
let gamepad = 0;
@@ -317,13 +321,12 @@ export default function MicroW8(screen, config = {}) {
}
canvasCtx.putImageData(imageData, 0, 0);
let timeOffset = ((time * 6) % 100 - 50) / 6;
thisFrame = startTime + time - timeOffset / 8;
} else {
thisFrame = Date.now();
let thisFrame = Math.floor(time * 6 / 100);
let timeOffset = time - thisFrame * 100 / 6;
nextFrame = Math.ceil(startTime + (thisFrame + 1) * 100 / 6 + Math.min(4, timeOffset));
}
let now = Date.now();
let nextFrame = Math.max(thisFrame + timePerFrame, now);
nextFrame = Math.max(nextFrame, now);
if (restart) {
runModule(currentData);