rewrite it in rust #1
			
				
			
		
		
		
	| 
						 | 
					@ -1,4 +1,5 @@
 | 
				
			||||||
/shrooms-vb
 | 
					/shrooms-vb
 | 
				
			||||||
/shrooms-vb.exe
 | 
					/shrooms-vb.exe
 | 
				
			||||||
.vscode
 | 
					.vscode
 | 
				
			||||||
output
 | 
					output
 | 
				
			||||||
 | 
					/target
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
					@ -0,0 +1,30 @@
 | 
				
			||||||
 | 
					[package]
 | 
				
			||||||
 | 
					name = "shrooms-vb"
 | 
				
			||||||
 | 
					version = "0.1.0"
 | 
				
			||||||
 | 
					edition = "2021"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dependencies]
 | 
				
			||||||
 | 
					anyhow = "1"
 | 
				
			||||||
 | 
					bitflags = "2"
 | 
				
			||||||
 | 
					bytemuck = { version = "1", features = ["derive"] }
 | 
				
			||||||
 | 
					clap = { version = "4", features = ["derive"] }
 | 
				
			||||||
 | 
					cpal = "0.15"
 | 
				
			||||||
 | 
					imgui = { version = "0.12", features = ["tables-api"] }
 | 
				
			||||||
 | 
					imgui-wgpu = { git = "https://github.com/Yatekii/imgui-wgpu-rs", rev = "2edd348" }
 | 
				
			||||||
 | 
					imgui-winit-support = "0.13"
 | 
				
			||||||
 | 
					itertools = "0.13"
 | 
				
			||||||
 | 
					native-dialog = "0.7"
 | 
				
			||||||
 | 
					num-derive = "0.4"
 | 
				
			||||||
 | 
					num-traits = "0.2"
 | 
				
			||||||
 | 
					pollster = "0.4"
 | 
				
			||||||
 | 
					rtrb = "0.3"
 | 
				
			||||||
 | 
					rubato = "0.16"
 | 
				
			||||||
 | 
					thread-priority = "1"
 | 
				
			||||||
 | 
					wgpu = "22.1"
 | 
				
			||||||
 | 
					winit = "0.30"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[build-dependencies]
 | 
				
			||||||
 | 
					cc = "1"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[profile.release]
 | 
				
			||||||
 | 
					lto = true
 | 
				
			||||||
							
								
								
									
										12
									
								
								README.md
								
								
								
								
							
							
						
						
									
										12
									
								
								README.md
								
								
								
								
							| 
						 | 
					@ -1,15 +1,15 @@
 | 
				
			||||||
# Shrooms VB (native)
 | 
					# Shrooms VB (native)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
An SDL-based implementation of shrooms-vb.
 | 
					A native implementation of shrooms-vb. Written in Rust, using winit, wgpu, and Dear ImGui. Should run on any major OS.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Setup
 | 
					## Setup
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Install the following dependencies:
 | 
					Install the following dependencies:
 | 
				
			||||||
 - `gcc` (or MinGW on Windows) (or whatever, just set `CC`)
 | 
					 - `cargo`
 | 
				
			||||||
 - `pkg-config`
 | 
					 | 
				
			||||||
 - sdl2
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
Run
 | 
					Run
 | 
				
			||||||
```sh
 | 
					```sh
 | 
				
			||||||
make build
 | 
					cargo build --release
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The executable will be in `target/release/shrooms-vb[.exe]`
 | 
				
			||||||
							
								
								
									
										12
									
								
								assets.h
								
								
								
								
							
							
						
						
									
										12
									
								
								assets.h
								
								
								
								
							| 
						 | 
					@ -1,12 +0,0 @@
 | 
				
			||||||
#ifndef SHROOMS_VB_NATIVE_ASSETS_
 | 
					 | 
				
			||||||
#define SHROOMS_VB_NATIVE_ASSETS_
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#include <stdint.h>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
extern const uint8_t _binary_assets_lefteye_bin_start;
 | 
					 | 
				
			||||||
const uint8_t *LEFT_EYE_DEFAULT = &_binary_assets_lefteye_bin_start; 
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
extern const uint8_t _binary_assets_righteye_bin_start;
 | 
					 | 
				
			||||||
const uint8_t *RIGHT_EYE_DEFAULT = &_binary_assets_righteye_bin_start; 
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#endif
 | 
					 | 
				
			||||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										65
									
								
								audio.c
								
								
								
								
							
							
						
						
									
										65
									
								
								audio.c
								
								
								
								
							| 
						 | 
					@ -1,65 +0,0 @@
 | 
				
			||||||
#include <audio.h>
 | 
					 | 
				
			||||||
#include <stdio.h>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
void audioCallback(void *userdata, uint8_t *stream, int len) {
 | 
					 | 
				
			||||||
    AudioContext *aud;
 | 
					 | 
				
			||||||
    SDL_assert(len == 834 * 4);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    aud = userdata;
 | 
					 | 
				
			||||||
    if (!aud->filled) {
 | 
					 | 
				
			||||||
        /* too little data, play silence */
 | 
					 | 
				
			||||||
        SDL_memset4(stream, 0, 834);
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    SDL_memcpy4(stream, aud->buffers[aud->current], 834);
 | 
					 | 
				
			||||||
    ++aud->current;
 | 
					 | 
				
			||||||
    aud->current %= 2;
 | 
					 | 
				
			||||||
    aud->filled -= 1;
 | 
					 | 
				
			||||||
} 
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
int audioInit(AudioContext *aud) {
 | 
					 | 
				
			||||||
    SDL_AudioSpec spec;
 | 
					 | 
				
			||||||
    spec.freq = 41700;
 | 
					 | 
				
			||||||
    spec.format = AUDIO_S16;
 | 
					 | 
				
			||||||
    spec.channels = 2;
 | 
					 | 
				
			||||||
    spec.samples = 834;
 | 
					 | 
				
			||||||
    spec.callback = &audioCallback;
 | 
					 | 
				
			||||||
    spec.userdata = aud;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    aud->id = SDL_OpenAudioDevice(NULL, 0, &spec, NULL, 0);
 | 
					 | 
				
			||||||
    aud->paused = true;
 | 
					 | 
				
			||||||
    if (!aud->id) {
 | 
					 | 
				
			||||||
        fprintf(stderr, "could not open audio device: %s\n", SDL_GetError());
 | 
					 | 
				
			||||||
        return -1;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    aud->current = 0;
 | 
					 | 
				
			||||||
    aud->filled = 0;
 | 
					 | 
				
			||||||
    return 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
int audioUpdate(AudioContext *aud, void *data, uint32_t bytes) {
 | 
					 | 
				
			||||||
    int filled;
 | 
					 | 
				
			||||||
    if (!aud->id) return -1;
 | 
					 | 
				
			||||||
    SDL_assert(bytes == 834 * 4);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    SDL_LockAudioDevice(aud->id);
 | 
					 | 
				
			||||||
    if (aud->filled < 2) {
 | 
					 | 
				
			||||||
        int next = (aud->current + aud->filled) % 2;
 | 
					 | 
				
			||||||
        SDL_memcpy4(aud->buffers[next], data, bytes / 4);
 | 
					 | 
				
			||||||
        aud->filled += 1;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    filled = aud->filled;
 | 
					 | 
				
			||||||
    SDL_UnlockAudioDevice(aud->id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (aud->paused) {
 | 
					 | 
				
			||||||
        SDL_PauseAudioDevice(aud->id, false);
 | 
					 | 
				
			||||||
        aud->paused = false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    while (filled > 1) {
 | 
					 | 
				
			||||||
        SDL_Delay(0);
 | 
					 | 
				
			||||||
        filled = aud->filled;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										18
									
								
								audio.h
								
								
								
								
							
							
						
						
									
										18
									
								
								audio.h
								
								
								
								
							| 
						 | 
					@ -1,18 +0,0 @@
 | 
				
			||||||
#ifndef SHROOMS_VB_NATIVE_AUDIO_
 | 
					 | 
				
			||||||
#define SHROOMS_VB_NATIVE_AUDIO_
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#include <SDL2/SDL.h>
 | 
					 | 
				
			||||||
#include <stdbool.h>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
typedef struct {
 | 
					 | 
				
			||||||
    SDL_AudioDeviceID id;
 | 
					 | 
				
			||||||
    bool paused;
 | 
					 | 
				
			||||||
    uint32_t buffers[2][834];
 | 
					 | 
				
			||||||
    int current;
 | 
					 | 
				
			||||||
    int filled;
 | 
					 | 
				
			||||||
} AudioContext;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
int audioInit(AudioContext *aud);
 | 
					 | 
				
			||||||
int audioUpdate(AudioContext *aud, void *data, uint32_t bytes);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#endif
 | 
					 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					use std::path::Path;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn main() {
 | 
				
			||||||
 | 
					    println!("cargo::rerun-if-changed=shrooms-vb-core");
 | 
				
			||||||
 | 
					    cc::Build::new()
 | 
				
			||||||
 | 
					        .include(Path::new("shrooms-vb-core/core"))
 | 
				
			||||||
 | 
					        .opt_level(2)
 | 
				
			||||||
 | 
					        .flag_if_supported("-flto")
 | 
				
			||||||
 | 
					        .flag_if_supported("-fno-strict-aliasing")
 | 
				
			||||||
 | 
					        .file(Path::new("shrooms-vb-core/core/vb.c"))
 | 
				
			||||||
 | 
					        .compile("vb");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								cli.c
								
								
								
								
							
							
						
						
									
										11
									
								
								cli.c
								
								
								
								
							| 
						 | 
					@ -1,11 +0,0 @@
 | 
				
			||||||
#include <cli.h>
 | 
					 | 
				
			||||||
#include <stdio.h>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
int parseCLIArgs(int argc, char **argv, CLIArgs *args) {
 | 
					 | 
				
			||||||
    if (argc != 2) {
 | 
					 | 
				
			||||||
        fprintf(stderr, "usage: %s /path/to/rom.vb\n", argv[0]);
 | 
					 | 
				
			||||||
        return 1;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    args->filename = argv[1];
 | 
					 | 
				
			||||||
    return 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										10
									
								
								cli.h
								
								
								
								
							
							
						
						
									
										10
									
								
								cli.h
								
								
								
								
							| 
						 | 
					@ -1,10 +0,0 @@
 | 
				
			||||||
#ifndef SHROOMS_VB_NATIVE_CLI_
 | 
					 | 
				
			||||||
#define SHROOMS_VB_NATIVE_CLI_
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
typedef struct {
 | 
					 | 
				
			||||||
    char *filename;
 | 
					 | 
				
			||||||
} CLIArgs;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
int parseCLIArgs(int argc, char **argv, CLIArgs *args);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#endif
 | 
					 | 
				
			||||||
							
								
								
									
										65
									
								
								controller.c
								
								
								
								
							
							
						
						
									
										65
									
								
								controller.c
								
								
								
								
							| 
						 | 
					@ -1,65 +0,0 @@
 | 
				
			||||||
#include <controller.h>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#define VB_PWR 0x0001
 | 
					 | 
				
			||||||
#define VB_SGN 0x0002
 | 
					 | 
				
			||||||
#define VB_A   0x0004
 | 
					 | 
				
			||||||
#define VB_B   0x0008
 | 
					 | 
				
			||||||
#define VB_RT  0x0010
 | 
					 | 
				
			||||||
#define VB_LT  0x0020
 | 
					 | 
				
			||||||
#define VB_RU  0x0040
 | 
					 | 
				
			||||||
#define VB_RR  0x0080
 | 
					 | 
				
			||||||
#define VB_LR  0x0100
 | 
					 | 
				
			||||||
#define VB_LL  0x0200
 | 
					 | 
				
			||||||
#define VB_LD  0x0400
 | 
					 | 
				
			||||||
#define VB_LU  0x0800
 | 
					 | 
				
			||||||
#define VB_STA 0x1000
 | 
					 | 
				
			||||||
#define VB_SEL 0x2000
 | 
					 | 
				
			||||||
#define VB_RL  0x4000
 | 
					 | 
				
			||||||
#define VB_RD  0x8000
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static uint16_t symToMask(SDL_KeyCode sym) {
 | 
					 | 
				
			||||||
    switch (sym) {
 | 
					 | 
				
			||||||
    default: return 0;
 | 
					 | 
				
			||||||
    case SDLK_a:
 | 
					 | 
				
			||||||
        return VB_SEL;
 | 
					 | 
				
			||||||
    case SDLK_s:
 | 
					 | 
				
			||||||
        return VB_STA;
 | 
					 | 
				
			||||||
    case SDLK_d:
 | 
					 | 
				
			||||||
        return VB_B;
 | 
					 | 
				
			||||||
    case SDLK_f:
 | 
					 | 
				
			||||||
        return VB_A;
 | 
					 | 
				
			||||||
    case SDLK_e:
 | 
					 | 
				
			||||||
        return VB_LT;
 | 
					 | 
				
			||||||
    case SDLK_r:
 | 
					 | 
				
			||||||
        return VB_RT;
 | 
					 | 
				
			||||||
    case SDLK_i:
 | 
					 | 
				
			||||||
        return VB_RU;
 | 
					 | 
				
			||||||
    case SDLK_j:
 | 
					 | 
				
			||||||
        return VB_RL;
 | 
					 | 
				
			||||||
    case SDLK_k:
 | 
					 | 
				
			||||||
        return VB_RD;
 | 
					 | 
				
			||||||
    case SDLK_l:
 | 
					 | 
				
			||||||
        return VB_RR;
 | 
					 | 
				
			||||||
    case SDLK_UP:
 | 
					 | 
				
			||||||
        return VB_LU;
 | 
					 | 
				
			||||||
    case SDLK_LEFT:
 | 
					 | 
				
			||||||
        return VB_LL;
 | 
					 | 
				
			||||||
    case SDLK_DOWN:
 | 
					 | 
				
			||||||
        return VB_LD;
 | 
					 | 
				
			||||||
    case SDLK_RIGHT:
 | 
					 | 
				
			||||||
        return VB_LR;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
void ctrlInit(ControllerState *ctrl) {
 | 
					 | 
				
			||||||
    ctrl->keys = VB_SGN;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
void ctrlKeyDown(ControllerState *ctrl, SDL_Keycode sym) {
 | 
					 | 
				
			||||||
    ctrl->keys |= symToMask(sym);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
void ctrlKeyUp(ControllerState *ctrl, SDL_Keycode sym) {
 | 
					 | 
				
			||||||
    ctrl->keys &= ~symToMask(sym);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
uint16_t ctrlKeys(ControllerState *ctrl) {
 | 
					 | 
				
			||||||
    return ctrl->keys;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										16
									
								
								controller.h
								
								
								
								
							
							
						
						
									
										16
									
								
								controller.h
								
								
								
								
							| 
						 | 
					@ -1,16 +0,0 @@
 | 
				
			||||||
#ifndef SHROOMS_VB_NATIVE_CONTROLLER_
 | 
					 | 
				
			||||||
#define SHROOMS_VB_NATIVE_CONTROLLER_
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#include <SDL2/SDL.h>
 | 
					 | 
				
			||||||
#include <stdint.h>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
typedef struct {
 | 
					 | 
				
			||||||
    uint16_t keys;
 | 
					 | 
				
			||||||
} ControllerState;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
void ctrlInit(ControllerState *ctrl);
 | 
					 | 
				
			||||||
void ctrlKeyDown(ControllerState *ctrl, SDL_Keycode sym);
 | 
					 | 
				
			||||||
void ctrlKeyUp(ControllerState *ctrl, SDL_Keycode sym);
 | 
					 | 
				
			||||||
uint16_t ctrlKeys(ControllerState *ctrl);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#endif
 | 
					 | 
				
			||||||
							
								
								
									
										71
									
								
								game.c
								
								
								
								
							
							
						
						
									
										71
									
								
								game.c
								
								
								
								
							| 
						 | 
					@ -1,71 +0,0 @@
 | 
				
			||||||
#include <audio.h>
 | 
					 | 
				
			||||||
#include <assets.h>
 | 
					 | 
				
			||||||
#include <controller.h>
 | 
					 | 
				
			||||||
#include <game.h>
 | 
					 | 
				
			||||||
#include <SDL2/SDL.h>
 | 
					 | 
				
			||||||
#include <stdbool.h>
 | 
					 | 
				
			||||||
#include <time.h>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
typedef struct {
 | 
					 | 
				
			||||||
    GraphicsContext *gfx;
 | 
					 | 
				
			||||||
    AudioContext aud;
 | 
					 | 
				
			||||||
    int16_t audioBuffer[834 * 2];
 | 
					 | 
				
			||||||
} GameState;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
int onFrame(VB *sim) {
 | 
					 | 
				
			||||||
    static uint8_t leftEye[384*224];
 | 
					 | 
				
			||||||
    static uint8_t rightEye[384*224];
 | 
					 | 
				
			||||||
    GameState *state;
 | 
					 | 
				
			||||||
    void *samples;
 | 
					 | 
				
			||||||
    uint32_t samplePairs;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    state = vbGetUserData(sim);
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    vbGetPixels(sim, leftEye, 1, 384, rightEye, 1, 384);
 | 
					 | 
				
			||||||
    gfxUpdateLeftEye(state->gfx, leftEye);
 | 
					 | 
				
			||||||
    gfxUpdateRightEye(state->gfx, rightEye);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    samples = vbGetSamples(sim, NULL, NULL, &samplePairs);
 | 
					 | 
				
			||||||
    audioUpdate(&state->aud, samples, samplePairs * 4);
 | 
					 | 
				
			||||||
    vbSetSamples(sim, samples, VB_S16, 834);
 | 
					 | 
				
			||||||
    gfxRender(state->gfx);
 | 
					 | 
				
			||||||
    return 1;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#define MAX_STEP_CLOCKS 20000000
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
int runGame(VB *sim, GraphicsContext *gfx) {
 | 
					 | 
				
			||||||
    uint32_t clocks;
 | 
					 | 
				
			||||||
    SDL_Event event;
 | 
					 | 
				
			||||||
    GameState state;
 | 
					 | 
				
			||||||
    ControllerState ctrl;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    state.gfx = gfx;
 | 
					 | 
				
			||||||
    audioInit(&state.aud);
 | 
					 | 
				
			||||||
    vbSetSamples(sim, &state.audioBuffer, VB_S16, 834);
 | 
					 | 
				
			||||||
    vbSetUserData(sim, &state);
 | 
					 | 
				
			||||||
    vbSetFrameCallback(sim, &onFrame);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    ctrlInit(&ctrl);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    gfxUpdateLeftEye(gfx, LEFT_EYE_DEFAULT);
 | 
					 | 
				
			||||||
    gfxUpdateRightEye(gfx, RIGHT_EYE_DEFAULT);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    while (1) {
 | 
					 | 
				
			||||||
        clocks = MAX_STEP_CLOCKS;
 | 
					 | 
				
			||||||
        vbEmulate(sim, &clocks);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        while (SDL_PollEvent(&event)) {
 | 
					 | 
				
			||||||
            if (event.type == SDL_QUIT) {
 | 
					 | 
				
			||||||
                return 0;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            if (event.type == SDL_KEYDOWN) {
 | 
					 | 
				
			||||||
                ctrlKeyDown(&ctrl, event.key.keysym.sym);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            if (event.type == SDL_KEYUP) {
 | 
					 | 
				
			||||||
                ctrlKeyUp(&ctrl, event.key.keysym.sym);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        vbSetKeys(sim, ctrlKeys(&ctrl));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										9
									
								
								game.h
								
								
								
								
							
							
						
						
									
										9
									
								
								game.h
								
								
								
								
							| 
						 | 
					@ -1,9 +0,0 @@
 | 
				
			||||||
#ifndef SHROOMS_VB_NATIVE_GAME_
 | 
					 | 
				
			||||||
#define SHROOMS_VB_NATIVE_GAME_
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#include "graphics.h"
 | 
					 | 
				
			||||||
#include "shrooms-vb-core/core/vb.h"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
int runGame(VB *sim, GraphicsContext *gfx);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#endif
 | 
					 | 
				
			||||||
							
								
								
									
										92
									
								
								graphics.c
								
								
								
								
							
							
						
						
									
										92
									
								
								graphics.c
								
								
								
								
							| 
						 | 
					@ -1,92 +0,0 @@
 | 
				
			||||||
#include <graphics.h>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void copyScreenTexture(uint8_t *dst, const uint8_t *src, int pitch) {
 | 
					 | 
				
			||||||
    int x, y, i;
 | 
					 | 
				
			||||||
    uint8_t color;
 | 
					 | 
				
			||||||
    int delta = pitch / 384;
 | 
					 | 
				
			||||||
    for (y = 0; y < 224; ++y) {
 | 
					 | 
				
			||||||
        for (x = 0; x < 384; x += 1) {
 | 
					 | 
				
			||||||
            color = src[(y * 384) + x];
 | 
					 | 
				
			||||||
            for (i = 0; i < delta; ++i) {
 | 
					 | 
				
			||||||
                dst[(y * pitch) + (x * delta) + i] = color;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
int gfxInit(GraphicsContext *gfx) {
 | 
					 | 
				
			||||||
    gfx->window = SDL_CreateWindow("Shrooms VB",
 | 
					 | 
				
			||||||
        SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
 | 
					 | 
				
			||||||
        1536, 896, SDL_WINDOW_SHOWN | SDL_WINDOW_ALLOW_HIGHDPI);
 | 
					 | 
				
			||||||
    if (!gfx->window) {
 | 
					 | 
				
			||||||
        fprintf(stderr, "Error creating window: %s\n", SDL_GetError());
 | 
					 | 
				
			||||||
        return 1;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    gfx->renderer = SDL_CreateRenderer(gfx->window, -1, 0);
 | 
					 | 
				
			||||||
    if (!gfx->renderer) {
 | 
					 | 
				
			||||||
        fprintf(stderr, "Error creating renderer: %s\n", SDL_GetError());
 | 
					 | 
				
			||||||
        goto cleanup_window;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    gfx->winSurface = SDL_GetWindowSurface(gfx->window);
 | 
					 | 
				
			||||||
    if (!gfx->winSurface) {
 | 
					 | 
				
			||||||
        fprintf(stderr, "Error getting surface: %s\n", SDL_GetError());
 | 
					 | 
				
			||||||
        goto cleanup_window;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    gfx->leftEye = SDL_CreateTexture(gfx->renderer, SDL_PIXELFORMAT_RGB24, SDL_TEXTUREACCESS_STREAMING, 384, 224);
 | 
					 | 
				
			||||||
    if (!gfx->leftEye) {
 | 
					 | 
				
			||||||
        fprintf(stderr, "Error creating left eye texture: %s\n", SDL_GetError());
 | 
					 | 
				
			||||||
        goto cleanup_window;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    SDL_SetTextureColorMod(gfx->leftEye, 0xff, 0, 0);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    gfx->rightEye = SDL_CreateTexture(gfx->renderer, SDL_PIXELFORMAT_RGB24, SDL_TEXTUREACCESS_STREAMING, 384, 224);
 | 
					 | 
				
			||||||
    if (!gfx->rightEye) {
 | 
					 | 
				
			||||||
        fprintf(stderr, "Error creating left eye texture: %s\n", SDL_GetError());
 | 
					 | 
				
			||||||
        goto cleanup_left_eye;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    SDL_SetTextureColorMod(gfx->rightEye, 0, 0xc6, 0xf0);
 | 
					 | 
				
			||||||
    SDL_SetTextureBlendMode(gfx->rightEye, SDL_BLENDMODE_ADD);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
cleanup_left_eye:
 | 
					 | 
				
			||||||
    SDL_DestroyTexture(gfx->leftEye);
 | 
					 | 
				
			||||||
cleanup_window:
 | 
					 | 
				
			||||||
    SDL_DestroyWindow(gfx->window);
 | 
					 | 
				
			||||||
    return 1;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
void gfxDestroy(GraphicsContext *gfx) {
 | 
					 | 
				
			||||||
    SDL_DestroyTexture(gfx->rightEye);
 | 
					 | 
				
			||||||
    SDL_DestroyTexture(gfx->leftEye);
 | 
					 | 
				
			||||||
    SDL_DestroyWindow(gfx->window);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void gfxUpdateEye(SDL_Texture *eye, const uint8_t *bytes) {
 | 
					 | 
				
			||||||
    void *target;
 | 
					 | 
				
			||||||
    int pitch;
 | 
					 | 
				
			||||||
    if (SDL_LockTexture(eye, NULL, &target, &pitch)) {
 | 
					 | 
				
			||||||
        fprintf(stderr, "Error locking buffer for eye: %s\n", SDL_GetError());
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    copyScreenTexture(target, bytes, pitch);
 | 
					 | 
				
			||||||
    SDL_UnlockTexture(eye);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
void gfxUpdateLeftEye(GraphicsContext *gfx, const uint8_t *bytes) {
 | 
					 | 
				
			||||||
    gfxUpdateEye(gfx->leftEye, bytes);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
void gfxUpdateRightEye(GraphicsContext *gfx, const uint8_t *bytes) {
 | 
					 | 
				
			||||||
    gfxUpdateEye(gfx->rightEye, bytes);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
void gfxRender(GraphicsContext *gfx) {
 | 
					 | 
				
			||||||
    SDL_RenderClear(gfx->renderer);
 | 
					 | 
				
			||||||
    SDL_RenderCopy(gfx->renderer, gfx->leftEye, NULL, NULL);
 | 
					 | 
				
			||||||
    SDL_RenderCopy(gfx->renderer, gfx->rightEye, NULL, NULL);
 | 
					 | 
				
			||||||
    SDL_RenderPresent(gfx->renderer);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
							
								
								
									
										22
									
								
								graphics.h
								
								
								
								
							
							
						
						
									
										22
									
								
								graphics.h
								
								
								
								
							| 
						 | 
					@ -1,22 +0,0 @@
 | 
				
			||||||
#ifndef SHROOMS_VB_NATIVE_GRAPHICS_
 | 
					 | 
				
			||||||
#define SHROOMS_VB_NATIVE_GRAPHICS_
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#include <SDL2/SDL.h>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
typedef struct {
 | 
					 | 
				
			||||||
    SDL_Window *window;
 | 
					 | 
				
			||||||
    SDL_Surface *winSurface;
 | 
					 | 
				
			||||||
    SDL_Renderer *renderer;
 | 
					 | 
				
			||||||
    SDL_Texture *leftEye;
 | 
					 | 
				
			||||||
    SDL_Texture *rightEye;
 | 
					 | 
				
			||||||
} GraphicsContext;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
int gfxInit(GraphicsContext *gfx);
 | 
					 | 
				
			||||||
void gfxDestroy(GraphicsContext *gfx);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
void gfxUpdateLeftEye(GraphicsContext *gfx, const uint8_t *bytes);
 | 
					 | 
				
			||||||
void gfxUpdateRightEye(GraphicsContext *gfx, const uint8_t *bytes);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
void gfxRender(GraphicsContext *gfx);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#endif
 | 
					 | 
				
			||||||
							
								
								
									
										85
									
								
								main.c
								
								
								
								
							
							
						
						
									
										85
									
								
								main.c
								
								
								
								
							| 
						 | 
					@ -1,85 +0,0 @@
 | 
				
			||||||
#include <cli.h>
 | 
					 | 
				
			||||||
#include <game.h>
 | 
					 | 
				
			||||||
#include <graphics.h>
 | 
					 | 
				
			||||||
#include <SDL2/SDL.h>
 | 
					 | 
				
			||||||
#include "shrooms-vb-core/core/vb.h"
 | 
					 | 
				
			||||||
#include <stdio.h>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
uint8_t *readROM(char *filename, uint32_t *size) {
 | 
					 | 
				
			||||||
    FILE *file = fopen(filename, "rb");
 | 
					 | 
				
			||||||
    uint8_t *rom;
 | 
					 | 
				
			||||||
    long filesize;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!file) {
 | 
					 | 
				
			||||||
        perror("could not open file");
 | 
					 | 
				
			||||||
        return NULL;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (fseek(file, 0, SEEK_END)) {
 | 
					 | 
				
			||||||
        perror("could not seek file end");
 | 
					 | 
				
			||||||
        return NULL;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    filesize = ftell(file);
 | 
					 | 
				
			||||||
    if (filesize == -1) {
 | 
					 | 
				
			||||||
        perror("could not read file size");
 | 
					 | 
				
			||||||
        return NULL;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (fseek(file, 0, SEEK_SET)) {
 | 
					 | 
				
			||||||
        perror("could not seek file start");
 | 
					 | 
				
			||||||
        return NULL;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    *size = (uint32_t) filesize;
 | 
					 | 
				
			||||||
    rom = malloc(*size);
 | 
					 | 
				
			||||||
    if (!rom) {
 | 
					 | 
				
			||||||
        perror("could not allocate ROM");
 | 
					 | 
				
			||||||
        return NULL;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    fread(rom, 1, *size, file);
 | 
					 | 
				
			||||||
    if (ferror(file)) {
 | 
					 | 
				
			||||||
        perror("could not read file");
 | 
					 | 
				
			||||||
        return NULL;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (fclose(file)) {
 | 
					 | 
				
			||||||
        perror("could not close file");
 | 
					 | 
				
			||||||
        return NULL;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return rom;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
int main(int argc, char **argv) {
 | 
					 | 
				
			||||||
    VB *sim;
 | 
					 | 
				
			||||||
    uint8_t *rom;
 | 
					 | 
				
			||||||
    uint32_t romSize;
 | 
					 | 
				
			||||||
    GraphicsContext gfx;
 | 
					 | 
				
			||||||
    CLIArgs args;
 | 
					 | 
				
			||||||
    int status;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (parseCLIArgs(argc, argv, &args)) {
 | 
					 | 
				
			||||||
        return 1;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    rom = readROM(args.filename, &romSize);
 | 
					 | 
				
			||||||
    if (!rom) {
 | 
					 | 
				
			||||||
        return 1;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    sim = malloc(vbSizeOf());
 | 
					 | 
				
			||||||
    vbInit(sim);
 | 
					 | 
				
			||||||
    vbSetCartROM(sim, rom, romSize);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (SDL_Init(SDL_INIT_EVERYTHING)) {
 | 
					 | 
				
			||||||
        fprintf(stderr, "Error initializing SDL: %s\n", SDL_GetError());
 | 
					 | 
				
			||||||
        return 1;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (gfxInit(&gfx)) {
 | 
					 | 
				
			||||||
        SDL_Quit();
 | 
					 | 
				
			||||||
        return 1;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    status = runGame(sim, &gfx);
 | 
					 | 
				
			||||||
    SDL_Quit();
 | 
					 | 
				
			||||||
    return status;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										48
									
								
								makefile
								
								
								
								
							
							
						
						
									
										48
									
								
								makefile
								
								
								
								
							| 
						 | 
					@ -1,48 +0,0 @@
 | 
				
			||||||
CC?=gcc
 | 
					 | 
				
			||||||
LD?=ld
 | 
					 | 
				
			||||||
SHROOMSFLAGS=shrooms-vb-core/core/vb.c -I shrooms-vb-core/core
 | 
					 | 
				
			||||||
msys_version := $(if $(findstring Msys, $(shell uname -o)),$(word 1, $(subst ., ,$(shell uname -r))),0)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ifeq ($(msys_version), 0)
 | 
					 | 
				
			||||||
SDL2FLAGS=$(shell pkg-config sdl2 --cflags --libs)
 | 
					 | 
				
			||||||
BINLINKFLAGS=-z noexecstack
 | 
					 | 
				
			||||||
else
 | 
					 | 
				
			||||||
SDL2FLAGS=$(shell pkg-config sdl2 --cflags --libs) -mwindows -mconsole
 | 
					 | 
				
			||||||
BINLINKFLAGS=
 | 
					 | 
				
			||||||
endif
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.PHONY: clean build
 | 
					 | 
				
			||||||
clean:
 | 
					 | 
				
			||||||
	@rm -rf shrooms-vb output
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
CFILES := $(foreach dir,./,$(notdir $(wildcard $(dir)/*.c)))
 | 
					 | 
				
			||||||
BINFILES := $(foreach dir,assets/,$(notdir $(wildcard $(dir)/*.bin)))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
COBJS := $(CFILES:%.c=output/%.o)
 | 
					 | 
				
			||||||
SHROOMSOBJS := output/vb.o
 | 
					 | 
				
			||||||
BINOBJS := $(BINFILES:%.bin=output/%.o)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
OFILES := $(COBJS) $(SHROOMSOBJS) $(BINOBJS)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
output/%.o: %.c
 | 
					 | 
				
			||||||
	@mkdir -p output
 | 
					 | 
				
			||||||
	@$(CC) -c -o $@ $< -I . \
 | 
					 | 
				
			||||||
		-I shrooms-vb-core/core $(SDL2FLAGS) \
 | 
					 | 
				
			||||||
		-O3 -flto -fno-strict-aliasing \
 | 
					 | 
				
			||||||
		-Werror -std=c90 -Wall -Wextra -Wpedantic
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
output/vb.o: shrooms-vb-core/core/vb.c
 | 
					 | 
				
			||||||
	@mkdir -p output
 | 
					 | 
				
			||||||
	@$(CC) -c -o $@ $< -I . \
 | 
					 | 
				
			||||||
		-I shrooms-vb-core/core $(SDL2FLAGS) \
 | 
					 | 
				
			||||||
		-O3 -flto -fno-strict-aliasing \
 | 
					 | 
				
			||||||
		-Werror -std=c90 -Wall -Wextra -Wpedantic
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
output/%.o: assets/%.bin
 | 
					 | 
				
			||||||
	@mkdir -p output
 | 
					 | 
				
			||||||
	@$(LD) -r -b binary $(BINLINKFLAGS) -o $@ $<
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
shrooms-vb: $(OFILES)
 | 
					 | 
				
			||||||
	@$(CC) -o $@ $(OFILES) $(SDL2FLAGS) -flto
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
build: shrooms-vb
 | 
					 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,55 @@
 | 
				
			||||||
 | 
					// Vertex shader
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct VertexOutput {
 | 
				
			||||||
 | 
					    @builtin(position) clip_position: vec4<f32>,
 | 
				
			||||||
 | 
					    @location(0) tex_coords: vec2<f32>,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@vertex
 | 
				
			||||||
 | 
					fn vs_main(
 | 
				
			||||||
 | 
					    @builtin(vertex_index) in_vertex_index: u32,
 | 
				
			||||||
 | 
					) -> VertexOutput {
 | 
				
			||||||
 | 
					    var out: VertexOutput;
 | 
				
			||||||
 | 
					    var x: f32;
 | 
				
			||||||
 | 
					    var y: f32;
 | 
				
			||||||
 | 
					    switch in_vertex_index {
 | 
				
			||||||
 | 
					        case 0u, 3u: {
 | 
				
			||||||
 | 
					            x = -1.0;
 | 
				
			||||||
 | 
					            y = 1.0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        case 1u: {
 | 
				
			||||||
 | 
					            x = -1.0;
 | 
				
			||||||
 | 
					            y = -1.0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        case 2u, 4u: {
 | 
				
			||||||
 | 
					            x = 1.0;
 | 
				
			||||||
 | 
					            y = -1.0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        default: {
 | 
				
			||||||
 | 
					            x = 1.0;
 | 
				
			||||||
 | 
					            y = 1.0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
 | 
				
			||||||
 | 
					    out.tex_coords = vec2<f32>((x + 1.0) / 2.0, (1.0 - y) / 2.0);
 | 
				
			||||||
 | 
					    return out;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Fragment shader
 | 
				
			||||||
 | 
					@group(0) @binding(0)
 | 
				
			||||||
 | 
					var u_texture: texture_2d<f32>;
 | 
				
			||||||
 | 
					@group(0) @binding(1)
 | 
				
			||||||
 | 
					var u_sampler: sampler;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct Colors {
 | 
				
			||||||
 | 
					    left: vec4<f32>,
 | 
				
			||||||
 | 
					    right: vec4<f32>,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					@group(0) @binding(2)
 | 
				
			||||||
 | 
					var<uniform> colors: Colors;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@fragment
 | 
				
			||||||
 | 
					fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
 | 
				
			||||||
 | 
					    let brt = textureSample(u_texture, u_sampler, in.tex_coords);
 | 
				
			||||||
 | 
					    return colors.left * brt[0] + colors.right * brt[1];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,131 @@
 | 
				
			||||||
 | 
					use std::{
 | 
				
			||||||
 | 
					    collections::HashMap,
 | 
				
			||||||
 | 
					    fmt::Debug,
 | 
				
			||||||
 | 
					    sync::{Arc, RwLock},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use game::GameWindow;
 | 
				
			||||||
 | 
					use winit::{
 | 
				
			||||||
 | 
					    application::ApplicationHandler,
 | 
				
			||||||
 | 
					    event::{Event, WindowEvent},
 | 
				
			||||||
 | 
					    event_loop::{ActiveEventLoop, EventLoopProxy},
 | 
				
			||||||
 | 
					    window::WindowId,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    controller::ControllerState,
 | 
				
			||||||
 | 
					    emulator::{EmulatorClient, EmulatorCommand},
 | 
				
			||||||
 | 
					    input::InputMapper,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					mod common;
 | 
				
			||||||
 | 
					mod game;
 | 
				
			||||||
 | 
					mod input;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct App {
 | 
				
			||||||
 | 
					    windows: HashMap<WindowId, Box<dyn AppWindow>>,
 | 
				
			||||||
 | 
					    client: EmulatorClient,
 | 
				
			||||||
 | 
					    input_mapper: Arc<RwLock<InputMapper>>,
 | 
				
			||||||
 | 
					    controller: ControllerState,
 | 
				
			||||||
 | 
					    proxy: EventLoopProxy<UserEvent>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl App {
 | 
				
			||||||
 | 
					    pub fn new(client: EmulatorClient, proxy: EventLoopProxy<UserEvent>) -> Self {
 | 
				
			||||||
 | 
					        let input_mapper = Arc::new(RwLock::new(InputMapper::new()));
 | 
				
			||||||
 | 
					        let controller = ControllerState::new(input_mapper.clone());
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            windows: HashMap::new(),
 | 
				
			||||||
 | 
					            client,
 | 
				
			||||||
 | 
					            input_mapper,
 | 
				
			||||||
 | 
					            controller,
 | 
				
			||||||
 | 
					            proxy,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ApplicationHandler<UserEvent> for App {
 | 
				
			||||||
 | 
					    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
 | 
				
			||||||
 | 
					        let mut window = GameWindow::new(
 | 
				
			||||||
 | 
					            event_loop,
 | 
				
			||||||
 | 
					            self.client.clone(),
 | 
				
			||||||
 | 
					            self.input_mapper.clone(),
 | 
				
			||||||
 | 
					            self.proxy.clone(),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        window.init();
 | 
				
			||||||
 | 
					        self.windows.insert(window.id(), Box::new(window));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn window_event(
 | 
				
			||||||
 | 
					        &mut self,
 | 
				
			||||||
 | 
					        event_loop: &ActiveEventLoop,
 | 
				
			||||||
 | 
					        window_id: WindowId,
 | 
				
			||||||
 | 
					        event: WindowEvent,
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					        if let WindowEvent::KeyboardInput { event, .. } = &event {
 | 
				
			||||||
 | 
					            if self.controller.key_event(event) {
 | 
				
			||||||
 | 
					                self.client
 | 
				
			||||||
 | 
					                    .send_command(EmulatorCommand::SetKeys(self.controller.pressed()));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        let Some(window) = self.windows.get_mut(&window_id) else {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        window.handle_event(event_loop, &Event::WindowEvent { window_id, event });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: UserEvent) {
 | 
				
			||||||
 | 
					        match event {
 | 
				
			||||||
 | 
					            UserEvent::OpenWindow(mut window) => {
 | 
				
			||||||
 | 
					                window.init();
 | 
				
			||||||
 | 
					                self.windows.insert(window.id(), window);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            UserEvent::CloseWindow(window_id) => {
 | 
				
			||||||
 | 
					                self.windows.remove(&window_id);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn device_event(
 | 
				
			||||||
 | 
					        &mut self,
 | 
				
			||||||
 | 
					        event_loop: &ActiveEventLoop,
 | 
				
			||||||
 | 
					        device_id: winit::event::DeviceId,
 | 
				
			||||||
 | 
					        event: winit::event::DeviceEvent,
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					        for window in self.windows.values_mut() {
 | 
				
			||||||
 | 
					            window.handle_event(
 | 
				
			||||||
 | 
					                event_loop,
 | 
				
			||||||
 | 
					                &Event::DeviceEvent {
 | 
				
			||||||
 | 
					                    device_id,
 | 
				
			||||||
 | 
					                    event: event.clone(),
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
 | 
				
			||||||
 | 
					        for window in self.windows.values_mut() {
 | 
				
			||||||
 | 
					            window.handle_event(event_loop, &Event::AboutToWait);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub trait AppWindow {
 | 
				
			||||||
 | 
					    fn id(&self) -> WindowId;
 | 
				
			||||||
 | 
					    fn init(&mut self);
 | 
				
			||||||
 | 
					    fn handle_event(&mut self, event_loop: &ActiveEventLoop, event: &Event<UserEvent>);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub enum UserEvent {
 | 
				
			||||||
 | 
					    OpenWindow(Box<dyn AppWindow>),
 | 
				
			||||||
 | 
					    CloseWindow(WindowId),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Debug for UserEvent {
 | 
				
			||||||
 | 
					    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            Self::OpenWindow(window) => f.debug_tuple("OpenWindow").field(&window.id()).finish(),
 | 
				
			||||||
 | 
					            Self::CloseWindow(window_id) => f.debug_tuple("CloseWindow").field(window_id).finish(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,264 @@
 | 
				
			||||||
 | 
					use std::{
 | 
				
			||||||
 | 
					    ops::{Deref, DerefMut},
 | 
				
			||||||
 | 
					    sync::Arc,
 | 
				
			||||||
 | 
					    time::Instant,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use imgui::{FontSource, MouseCursor, SuspendedContext, WindowToken};
 | 
				
			||||||
 | 
					use imgui_wgpu::{Renderer, RendererConfig};
 | 
				
			||||||
 | 
					use imgui_winit_support::WinitPlatform;
 | 
				
			||||||
 | 
					use pollster::block_on;
 | 
				
			||||||
 | 
					#[cfg(target_os = "windows")]
 | 
				
			||||||
 | 
					use winit::platform::windows::{CornerPreference, WindowAttributesExtWindows as _};
 | 
				
			||||||
 | 
					use winit::{
 | 
				
			||||||
 | 
					    dpi::{LogicalSize, PhysicalSize, Size},
 | 
				
			||||||
 | 
					    event_loop::ActiveEventLoop,
 | 
				
			||||||
 | 
					    window::{Window, WindowAttributes},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct WindowStateBuilder<'a> {
 | 
				
			||||||
 | 
					    event_loop: &'a ActiveEventLoop,
 | 
				
			||||||
 | 
					    attributes: WindowAttributes,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					impl<'a> WindowStateBuilder<'a> {
 | 
				
			||||||
 | 
					    pub fn new(event_loop: &'a ActiveEventLoop) -> Self {
 | 
				
			||||||
 | 
					        let attributes = Window::default_attributes();
 | 
				
			||||||
 | 
					        #[cfg(target_os = "windows")]
 | 
				
			||||||
 | 
					        let attributes = attributes.with_corner_preference(CornerPreference::DoNotRound);
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            event_loop,
 | 
				
			||||||
 | 
					            attributes,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn with_title<T: Into<String>>(self, title: T) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            attributes: self.attributes.with_title(title),
 | 
				
			||||||
 | 
					            ..self
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn with_inner_size<S: Into<Size>>(self, size: S) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            attributes: self.attributes.with_inner_size(size),
 | 
				
			||||||
 | 
					            ..self
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn build(self) -> WindowState {
 | 
				
			||||||
 | 
					        WindowState::new(self.event_loop, self.attributes)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					pub struct WindowState {
 | 
				
			||||||
 | 
					    pub device: wgpu::Device,
 | 
				
			||||||
 | 
					    pub queue: Arc<wgpu::Queue>,
 | 
				
			||||||
 | 
					    pub window: Arc<Window>,
 | 
				
			||||||
 | 
					    pub surface_desc: wgpu::SurfaceConfiguration,
 | 
				
			||||||
 | 
					    pub surface: wgpu::Surface<'static>,
 | 
				
			||||||
 | 
					    pub hidpi_factor: f64,
 | 
				
			||||||
 | 
					    pub minimized: bool,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl WindowState {
 | 
				
			||||||
 | 
					    fn new(event_loop: &ActiveEventLoop, attributes: WindowAttributes) -> Self {
 | 
				
			||||||
 | 
					        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
 | 
				
			||||||
 | 
					            backends: wgpu::Backends::PRIMARY,
 | 
				
			||||||
 | 
					            ..Default::default()
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let window = Arc::new(event_loop.create_window(attributes).unwrap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let size = window.inner_size();
 | 
				
			||||||
 | 
					        let hidpi_factor = window.scale_factor();
 | 
				
			||||||
 | 
					        let surface = instance.create_surface(window.clone()).unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
 | 
				
			||||||
 | 
					            power_preference: wgpu::PowerPreference::HighPerformance,
 | 
				
			||||||
 | 
					            compatible_surface: Some(&surface),
 | 
				
			||||||
 | 
					            force_fallback_adapter: false,
 | 
				
			||||||
 | 
					        }))
 | 
				
			||||||
 | 
					        .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let (device, queue) =
 | 
				
			||||||
 | 
					            block_on(adapter.request_device(&wgpu::DeviceDescriptor::default(), None)).unwrap();
 | 
				
			||||||
 | 
					        let queue = Arc::new(queue);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Set up swap chain
 | 
				
			||||||
 | 
					        let surface_desc = wgpu::SurfaceConfiguration {
 | 
				
			||||||
 | 
					            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
 | 
				
			||||||
 | 
					            format: wgpu::TextureFormat::Bgra8UnormSrgb,
 | 
				
			||||||
 | 
					            width: size.width,
 | 
				
			||||||
 | 
					            height: size.height,
 | 
				
			||||||
 | 
					            present_mode: wgpu::PresentMode::Fifo,
 | 
				
			||||||
 | 
					            desired_maximum_frame_latency: 2,
 | 
				
			||||||
 | 
					            alpha_mode: wgpu::CompositeAlphaMode::Auto,
 | 
				
			||||||
 | 
					            view_formats: vec![wgpu::TextureFormat::Bgra8Unorm],
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        surface.configure(&device, &surface_desc);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            device,
 | 
				
			||||||
 | 
					            queue,
 | 
				
			||||||
 | 
					            window,
 | 
				
			||||||
 | 
					            surface_desc,
 | 
				
			||||||
 | 
					            surface,
 | 
				
			||||||
 | 
					            hidpi_factor,
 | 
				
			||||||
 | 
					            minimized: false,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn logical_size(&self) -> LogicalSize<u32> {
 | 
				
			||||||
 | 
					        PhysicalSize::new(self.surface_desc.width, self.surface_desc.height)
 | 
				
			||||||
 | 
					            .to_logical(self.hidpi_factor)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn handle_resize(&mut self, size: &PhysicalSize<u32>) {
 | 
				
			||||||
 | 
					        if size.width > 0 && size.height > 0 {
 | 
				
			||||||
 | 
					            self.minimized = false;
 | 
				
			||||||
 | 
					            self.surface_desc.width = size.width;
 | 
				
			||||||
 | 
					            self.surface_desc.height = size.height;
 | 
				
			||||||
 | 
					            self.surface.configure(&self.device, &self.surface_desc);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            self.minimized = true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct ImguiState {
 | 
				
			||||||
 | 
					    pub context: ContextGuard,
 | 
				
			||||||
 | 
					    pub platform: WinitPlatform,
 | 
				
			||||||
 | 
					    pub renderer: Renderer,
 | 
				
			||||||
 | 
					    pub clear_color: wgpu::Color,
 | 
				
			||||||
 | 
					    pub last_frame: Instant,
 | 
				
			||||||
 | 
					    pub last_cursor: Option<MouseCursor>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					impl ImguiState {
 | 
				
			||||||
 | 
					    pub fn new(window: &WindowState) -> Self {
 | 
				
			||||||
 | 
					        let mut context_guard = ContextGuard::new();
 | 
				
			||||||
 | 
					        let mut context = context_guard.lock().unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let mut platform = imgui_winit_support::WinitPlatform::new(&mut context);
 | 
				
			||||||
 | 
					        platform.attach_window(
 | 
				
			||||||
 | 
					            context.io_mut(),
 | 
				
			||||||
 | 
					            &window.window,
 | 
				
			||||||
 | 
					            imgui_winit_support::HiDpiMode::Default,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        context.set_ini_filename(None);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let font_size = (16.0 * window.hidpi_factor) as f32;
 | 
				
			||||||
 | 
					        context.io_mut().font_global_scale = (1.0 / window.hidpi_factor) as f32;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        context.fonts().add_font(&[FontSource::TtfData {
 | 
				
			||||||
 | 
					            data: include_bytes!("../../assets/selawk.ttf"),
 | 
				
			||||||
 | 
					            size_pixels: font_size,
 | 
				
			||||||
 | 
					            config: Some(imgui::FontConfig {
 | 
				
			||||||
 | 
					                oversample_h: 1,
 | 
				
			||||||
 | 
					                pixel_snap_h: true,
 | 
				
			||||||
 | 
					                size_pixels: font_size,
 | 
				
			||||||
 | 
					                ..Default::default()
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					        }]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let style = context.style_mut();
 | 
				
			||||||
 | 
					        style.use_light_colors();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        //
 | 
				
			||||||
 | 
					        // Set up dear imgui wgpu renderer
 | 
				
			||||||
 | 
					        //
 | 
				
			||||||
 | 
					        let renderer_config = RendererConfig {
 | 
				
			||||||
 | 
					            texture_format: window.surface_desc.format,
 | 
				
			||||||
 | 
					            ..Default::default()
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let renderer = Renderer::new(&mut context, &window.device, &window.queue, renderer_config);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let last_frame = Instant::now();
 | 
				
			||||||
 | 
					        let last_cursor = None;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        drop(context);
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            context: context_guard,
 | 
				
			||||||
 | 
					            platform,
 | 
				
			||||||
 | 
					            renderer,
 | 
				
			||||||
 | 
					            clear_color: wgpu::Color::BLACK,
 | 
				
			||||||
 | 
					            last_frame,
 | 
				
			||||||
 | 
					            last_cursor,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct ContextGuard {
 | 
				
			||||||
 | 
					    value: Option<SuspendedContext>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ContextGuard {
 | 
				
			||||||
 | 
					    fn new() -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            value: Some(SuspendedContext::create()),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn lock(&mut self) -> Option<ContextLock<'_>> {
 | 
				
			||||||
 | 
					        let sus = self.value.take()?;
 | 
				
			||||||
 | 
					        match sus.activate() {
 | 
				
			||||||
 | 
					            Ok(ctx) => Some(ContextLock {
 | 
				
			||||||
 | 
					                ctx: Some(ctx),
 | 
				
			||||||
 | 
					                holder: self,
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            Err(sus) => {
 | 
				
			||||||
 | 
					                self.value = Some(sus);
 | 
				
			||||||
 | 
					                None
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct ContextLock<'a> {
 | 
				
			||||||
 | 
					    ctx: Option<imgui::Context>,
 | 
				
			||||||
 | 
					    holder: &'a mut ContextGuard,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'a> Deref for ContextLock<'a> {
 | 
				
			||||||
 | 
					    type Target = imgui::Context;
 | 
				
			||||||
 | 
					    fn deref(&self) -> &Self::Target {
 | 
				
			||||||
 | 
					        self.ctx.as_ref().unwrap()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'a> DerefMut for ContextLock<'a> {
 | 
				
			||||||
 | 
					    fn deref_mut(&mut self) -> &mut Self::Target {
 | 
				
			||||||
 | 
					        self.ctx.as_mut().unwrap()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'a> Drop for ContextLock<'a> {
 | 
				
			||||||
 | 
					    fn drop(&mut self) {
 | 
				
			||||||
 | 
					        self.holder.value = self.ctx.take().map(|c| c.suspend())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub trait UiExt {
 | 
				
			||||||
 | 
					    fn fullscreen_window(&self) -> Option<WindowToken<'_>>;
 | 
				
			||||||
 | 
					    fn right_align_text<T: AsRef<str>>(&self, text: T, space: f32);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl UiExt for imgui::Ui {
 | 
				
			||||||
 | 
					    fn fullscreen_window(&self) -> Option<WindowToken<'_>> {
 | 
				
			||||||
 | 
					        self.window("fullscreen")
 | 
				
			||||||
 | 
					            .position([0.0, 0.0], imgui::Condition::Always)
 | 
				
			||||||
 | 
					            .size(self.io().display_size, imgui::Condition::Always)
 | 
				
			||||||
 | 
					            .flags(imgui::WindowFlags::NO_DECORATION)
 | 
				
			||||||
 | 
					            .begin()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn right_align_text<T: AsRef<str>>(&self, text: T, space: f32) {
 | 
				
			||||||
 | 
					        let width = self.calc_text_size(text.as_ref())[0];
 | 
				
			||||||
 | 
					        let [left, y] = self.cursor_pos();
 | 
				
			||||||
 | 
					        let right = left + space;
 | 
				
			||||||
 | 
					        self.set_cursor_pos([right - width, y]);
 | 
				
			||||||
 | 
					        self.text(text);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,384 @@
 | 
				
			||||||
 | 
					use std::{
 | 
				
			||||||
 | 
					    sync::{Arc, RwLock},
 | 
				
			||||||
 | 
					    time::Instant,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use wgpu::util::DeviceExt as _;
 | 
				
			||||||
 | 
					use winit::{
 | 
				
			||||||
 | 
					    dpi::LogicalSize,
 | 
				
			||||||
 | 
					    event::{Event, WindowEvent},
 | 
				
			||||||
 | 
					    event_loop::{ActiveEventLoop, EventLoopProxy},
 | 
				
			||||||
 | 
					    window::WindowId,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    emulator::{EmulatorClient, EmulatorCommand},
 | 
				
			||||||
 | 
					    input::InputMapper,
 | 
				
			||||||
 | 
					    renderer::GameRenderer,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::{
 | 
				
			||||||
 | 
					    common::{ImguiState, WindowState, WindowStateBuilder},
 | 
				
			||||||
 | 
					    input::InputWindow,
 | 
				
			||||||
 | 
					    AppWindow, UserEvent,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct GameWindow {
 | 
				
			||||||
 | 
					    window: WindowState,
 | 
				
			||||||
 | 
					    imgui: Option<ImguiState>,
 | 
				
			||||||
 | 
					    pipeline: wgpu::RenderPipeline,
 | 
				
			||||||
 | 
					    bind_group: wgpu::BindGroup,
 | 
				
			||||||
 | 
					    client: EmulatorClient,
 | 
				
			||||||
 | 
					    input_mapper: Arc<RwLock<InputMapper>>,
 | 
				
			||||||
 | 
					    proxy: EventLoopProxy<UserEvent>,
 | 
				
			||||||
 | 
					    paused_due_to_minimize: bool,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl GameWindow {
 | 
				
			||||||
 | 
					    pub fn new(
 | 
				
			||||||
 | 
					        event_loop: &ActiveEventLoop,
 | 
				
			||||||
 | 
					        client: EmulatorClient,
 | 
				
			||||||
 | 
					        input_mapper: Arc<RwLock<InputMapper>>,
 | 
				
			||||||
 | 
					        proxy: EventLoopProxy<UserEvent>,
 | 
				
			||||||
 | 
					    ) -> Self {
 | 
				
			||||||
 | 
					        let window = WindowStateBuilder::new(event_loop)
 | 
				
			||||||
 | 
					            .with_title("Shrooms VB")
 | 
				
			||||||
 | 
					            .with_inner_size(LogicalSize::new(384, 244))
 | 
				
			||||||
 | 
					            .build();
 | 
				
			||||||
 | 
					        let device = &window.device;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let eyes = Arc::new(GameRenderer::create_texture(device, "eye"));
 | 
				
			||||||
 | 
					        client.send_command(EmulatorCommand::SetRenderer(GameRenderer {
 | 
				
			||||||
 | 
					            queue: window.queue.clone(),
 | 
				
			||||||
 | 
					            eyes: eyes.clone(),
 | 
				
			||||||
 | 
					        }));
 | 
				
			||||||
 | 
					        let eyes = eyes.create_view(&wgpu::TextureViewDescriptor::default());
 | 
				
			||||||
 | 
					        let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default());
 | 
				
			||||||
 | 
					        let colors = Colors {
 | 
				
			||||||
 | 
					            left: [1.0, 0.0, 0.0, 1.0],
 | 
				
			||||||
 | 
					            right: [0.0, 0.7734375, 0.9375, 1.0],
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        let color_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
 | 
				
			||||||
 | 
					            label: Some("colors"),
 | 
				
			||||||
 | 
					            contents: bytemuck::bytes_of(&colors),
 | 
				
			||||||
 | 
					            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        let texture_bind_group_layout =
 | 
				
			||||||
 | 
					            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
 | 
				
			||||||
 | 
					                label: Some("texture bind group layout"),
 | 
				
			||||||
 | 
					                entries: &[
 | 
				
			||||||
 | 
					                    wgpu::BindGroupLayoutEntry {
 | 
				
			||||||
 | 
					                        binding: 0,
 | 
				
			||||||
 | 
					                        visibility: wgpu::ShaderStages::FRAGMENT,
 | 
				
			||||||
 | 
					                        ty: wgpu::BindingType::Texture {
 | 
				
			||||||
 | 
					                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
 | 
				
			||||||
 | 
					                            view_dimension: wgpu::TextureViewDimension::D2,
 | 
				
			||||||
 | 
					                            multisampled: false,
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                        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::FRAGMENT,
 | 
				
			||||||
 | 
					                        ty: wgpu::BindingType::Buffer {
 | 
				
			||||||
 | 
					                            ty: wgpu::BufferBindingType::Uniform,
 | 
				
			||||||
 | 
					                            has_dynamic_offset: false,
 | 
				
			||||||
 | 
					                            min_binding_size: None,
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                        count: None,
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
 | 
				
			||||||
 | 
					            label: Some("bind group"),
 | 
				
			||||||
 | 
					            layout: &texture_bind_group_layout,
 | 
				
			||||||
 | 
					            entries: &[
 | 
				
			||||||
 | 
					                wgpu::BindGroupEntry {
 | 
				
			||||||
 | 
					                    binding: 0,
 | 
				
			||||||
 | 
					                    resource: wgpu::BindingResource::TextureView(&eyes),
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                wgpu::BindGroupEntry {
 | 
				
			||||||
 | 
					                    binding: 1,
 | 
				
			||||||
 | 
					                    resource: wgpu::BindingResource::Sampler(&sampler),
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                wgpu::BindGroupEntry {
 | 
				
			||||||
 | 
					                    binding: 2,
 | 
				
			||||||
 | 
					                    resource: color_buf.as_entire_binding(),
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let shader = device.create_shader_module(wgpu::include_wgsl!("../anaglyph.wgsl"));
 | 
				
			||||||
 | 
					        let render_pipeline_layout =
 | 
				
			||||||
 | 
					            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
 | 
				
			||||||
 | 
					                label: Some("render pipeline layout"),
 | 
				
			||||||
 | 
					                bind_group_layouts: &[&texture_bind_group_layout],
 | 
				
			||||||
 | 
					                push_constant_ranges: &[],
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
 | 
				
			||||||
 | 
					            label: Some("render pipeline"),
 | 
				
			||||||
 | 
					            layout: Some(&render_pipeline_layout),
 | 
				
			||||||
 | 
					            vertex: wgpu::VertexState {
 | 
				
			||||||
 | 
					                module: &shader,
 | 
				
			||||||
 | 
					                entry_point: "vs_main",
 | 
				
			||||||
 | 
					                buffers: &[],
 | 
				
			||||||
 | 
					                compilation_options: wgpu::PipelineCompilationOptions::default(),
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            fragment: Some(wgpu::FragmentState {
 | 
				
			||||||
 | 
					                module: &shader,
 | 
				
			||||||
 | 
					                entry_point: "fs_main",
 | 
				
			||||||
 | 
					                targets: &[Some(wgpu::ColorTargetState {
 | 
				
			||||||
 | 
					                    format: wgpu::TextureFormat::Bgra8UnormSrgb,
 | 
				
			||||||
 | 
					                    blend: Some(wgpu::BlendState::REPLACE),
 | 
				
			||||||
 | 
					                    write_mask: wgpu::ColorWrites::ALL,
 | 
				
			||||||
 | 
					                })],
 | 
				
			||||||
 | 
					                compilation_options: wgpu::PipelineCompilationOptions::default(),
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            primitive: wgpu::PrimitiveState {
 | 
				
			||||||
 | 
					                topology: wgpu::PrimitiveTopology::TriangleList,
 | 
				
			||||||
 | 
					                strip_index_format: None,
 | 
				
			||||||
 | 
					                front_face: wgpu::FrontFace::Ccw,
 | 
				
			||||||
 | 
					                cull_mode: Some(wgpu::Face::Back),
 | 
				
			||||||
 | 
					                polygon_mode: wgpu::PolygonMode::Fill,
 | 
				
			||||||
 | 
					                unclipped_depth: false,
 | 
				
			||||||
 | 
					                conservative: false,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            depth_stencil: None,
 | 
				
			||||||
 | 
					            multisample: wgpu::MultisampleState {
 | 
				
			||||||
 | 
					                count: 1,
 | 
				
			||||||
 | 
					                mask: !0,
 | 
				
			||||||
 | 
					                alpha_to_coverage_enabled: false,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            multiview: None,
 | 
				
			||||||
 | 
					            cache: None,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            window,
 | 
				
			||||||
 | 
					            imgui: None,
 | 
				
			||||||
 | 
					            pipeline: render_pipeline,
 | 
				
			||||||
 | 
					            bind_group,
 | 
				
			||||||
 | 
					            client,
 | 
				
			||||||
 | 
					            input_mapper,
 | 
				
			||||||
 | 
					            proxy,
 | 
				
			||||||
 | 
					            paused_due_to_minimize: false,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn draw(&mut self, event_loop: &ActiveEventLoop) {
 | 
				
			||||||
 | 
					        let window = &mut self.window;
 | 
				
			||||||
 | 
					        let imgui = self.imgui.as_mut().unwrap();
 | 
				
			||||||
 | 
					        let mut context = imgui.context.lock().unwrap();
 | 
				
			||||||
 | 
					        let mut new_size = None;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let now = Instant::now();
 | 
				
			||||||
 | 
					        context.io_mut().update_delta_time(now - imgui.last_frame);
 | 
				
			||||||
 | 
					        imgui.last_frame = now;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let frame = match window.surface.get_current_texture() {
 | 
				
			||||||
 | 
					            Ok(frame) => frame,
 | 
				
			||||||
 | 
					            Err(e) => {
 | 
				
			||||||
 | 
					                if !self.window.minimized {
 | 
				
			||||||
 | 
					                    eprintln!("dropped frame: {e:?}");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        imgui
 | 
				
			||||||
 | 
					            .platform
 | 
				
			||||||
 | 
					            .prepare_frame(context.io_mut(), &window.window)
 | 
				
			||||||
 | 
					            .expect("Failed to prepare frame");
 | 
				
			||||||
 | 
					        let ui = context.new_frame();
 | 
				
			||||||
 | 
					        let mut menu_height = 0.0;
 | 
				
			||||||
 | 
					        ui.main_menu_bar(|| {
 | 
				
			||||||
 | 
					            menu_height = ui.window_size()[1];
 | 
				
			||||||
 | 
					            ui.menu("ROM", || {
 | 
				
			||||||
 | 
					                if ui.menu_item("Open ROM") {
 | 
				
			||||||
 | 
					                    let rom = native_dialog::FileDialog::new()
 | 
				
			||||||
 | 
					                        .add_filter("Virtual Boy ROMs", &["vb", "vbrom"])
 | 
				
			||||||
 | 
					                        .show_open_single_file()
 | 
				
			||||||
 | 
					                        .unwrap();
 | 
				
			||||||
 | 
					                    if let Some(path) = rom {
 | 
				
			||||||
 | 
					                        self.client.send_command(EmulatorCommand::LoadGame(path));
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if ui.menu_item("Quit") {
 | 
				
			||||||
 | 
					                    event_loop.exit();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            ui.menu("Emulation", || {
 | 
				
			||||||
 | 
					                let has_game = self.client.has_game();
 | 
				
			||||||
 | 
					                if self.client.is_running() {
 | 
				
			||||||
 | 
					                    if ui.menu_item_config("Pause").enabled(has_game).build() {
 | 
				
			||||||
 | 
					                        self.client.send_command(EmulatorCommand::Pause);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } else if ui.menu_item_config("Resume").enabled(has_game).build() {
 | 
				
			||||||
 | 
					                    self.client.send_command(EmulatorCommand::Resume);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if ui.menu_item_config("Reset").enabled(has_game).build() {
 | 
				
			||||||
 | 
					                    self.client.send_command(EmulatorCommand::Reset);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            ui.menu("Video", || {
 | 
				
			||||||
 | 
					                let current_dims = window.logical_size();
 | 
				
			||||||
 | 
					                for scale in 1..=4 {
 | 
				
			||||||
 | 
					                    let label = format!("x{scale}");
 | 
				
			||||||
 | 
					                    let dims = LogicalSize::new(384 * scale, 224 * scale + 20);
 | 
				
			||||||
 | 
					                    let selected = dims == current_dims;
 | 
				
			||||||
 | 
					                    if ui.menu_item_config(label).selected(selected).build() {
 | 
				
			||||||
 | 
					                        if let Some(size) = window.window.request_inner_size(dims) {
 | 
				
			||||||
 | 
					                            window.handle_resize(&size);
 | 
				
			||||||
 | 
					                            new_size = Some(size);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            ui.menu("Input", || {
 | 
				
			||||||
 | 
					                if ui.menu_item("Bind Inputs") {
 | 
				
			||||||
 | 
					                    let input_window = Box::new(InputWindow::new(
 | 
				
			||||||
 | 
					                        event_loop,
 | 
				
			||||||
 | 
					                        self.input_mapper.clone(),
 | 
				
			||||||
 | 
					                        self.proxy.clone(),
 | 
				
			||||||
 | 
					                    ));
 | 
				
			||||||
 | 
					                    self.proxy
 | 
				
			||||||
 | 
					                        .send_event(UserEvent::OpenWindow(input_window))
 | 
				
			||||||
 | 
					                        .unwrap();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let mut encoder: wgpu::CommandEncoder = window
 | 
				
			||||||
 | 
					            .device
 | 
				
			||||||
 | 
					            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if imgui.last_cursor != ui.mouse_cursor() {
 | 
				
			||||||
 | 
					            imgui.last_cursor = ui.mouse_cursor();
 | 
				
			||||||
 | 
					            imgui.platform.prepare_render(ui, &window.window);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let view = frame
 | 
				
			||||||
 | 
					            .texture
 | 
				
			||||||
 | 
					            .create_view(&wgpu::TextureViewDescriptor::default());
 | 
				
			||||||
 | 
					        let mut rpass = 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(imgui.clear_color),
 | 
				
			||||||
 | 
					                    store: wgpu::StoreOp::Store,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            })],
 | 
				
			||||||
 | 
					            depth_stencil_attachment: None,
 | 
				
			||||||
 | 
					            timestamp_writes: None,
 | 
				
			||||||
 | 
					            occlusion_query_set: None,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Draw the game
 | 
				
			||||||
 | 
					        rpass.set_pipeline(&self.pipeline);
 | 
				
			||||||
 | 
					        let window_width = window.surface_desc.width as f32;
 | 
				
			||||||
 | 
					        let window_height = window.surface_desc.height as f32;
 | 
				
			||||||
 | 
					        let menu_height = menu_height * window.hidpi_factor as f32;
 | 
				
			||||||
 | 
					        let ((x, y), (width, height)) =
 | 
				
			||||||
 | 
					            compute_game_bounds(window_width, window_height, menu_height);
 | 
				
			||||||
 | 
					        rpass.set_viewport(x, y, width, height, 0.0, 1.0);
 | 
				
			||||||
 | 
					        rpass.set_bind_group(0, &self.bind_group, &[]);
 | 
				
			||||||
 | 
					        rpass.draw(0..6, 0..1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Draw the menu on top of the game
 | 
				
			||||||
 | 
					        rpass.set_viewport(0.0, 0.0, window_width, window_height, 0.0, 1.0);
 | 
				
			||||||
 | 
					        imgui
 | 
				
			||||||
 | 
					            .renderer
 | 
				
			||||||
 | 
					            .render(context.render(), &window.queue, &window.device, &mut rpass)
 | 
				
			||||||
 | 
					            .expect("Rendering failed");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        drop(rpass);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Some(size) = new_size {
 | 
				
			||||||
 | 
					            imgui.platform.handle_event::<UserEvent>(
 | 
				
			||||||
 | 
					                context.io_mut(),
 | 
				
			||||||
 | 
					                &window.window,
 | 
				
			||||||
 | 
					                &Event::WindowEvent {
 | 
				
			||||||
 | 
					                    window_id: window.window.id(),
 | 
				
			||||||
 | 
					                    event: WindowEvent::Resized(size),
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        window.queue.submit(Some(encoder.finish()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        frame.present();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl AppWindow for GameWindow {
 | 
				
			||||||
 | 
					    fn id(&self) -> WindowId {
 | 
				
			||||||
 | 
					        self.window.window.id()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn init(&mut self) {
 | 
				
			||||||
 | 
					        self.imgui = Some(ImguiState::new(&self.window));
 | 
				
			||||||
 | 
					        self.window.window.request_redraw();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn handle_event(&mut self, event_loop: &ActiveEventLoop, event: &Event<UserEvent>) {
 | 
				
			||||||
 | 
					        match event {
 | 
				
			||||||
 | 
					            Event::WindowEvent { event, .. } => match event {
 | 
				
			||||||
 | 
					                WindowEvent::Resized(size) => {
 | 
				
			||||||
 | 
					                    self.window.handle_resize(size);
 | 
				
			||||||
 | 
					                    if self.window.minimized {
 | 
				
			||||||
 | 
					                        if self.client.is_running() {
 | 
				
			||||||
 | 
					                            self.client.send_command(EmulatorCommand::Pause);
 | 
				
			||||||
 | 
					                            self.paused_due_to_minimize = true;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    } else if self.paused_due_to_minimize {
 | 
				
			||||||
 | 
					                        self.client.send_command(EmulatorCommand::Resume);
 | 
				
			||||||
 | 
					                        self.paused_due_to_minimize = false;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                WindowEvent::CloseRequested => event_loop.exit(),
 | 
				
			||||||
 | 
					                WindowEvent::RedrawRequested => self.draw(event_loop),
 | 
				
			||||||
 | 
					                _ => (),
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            Event::AboutToWait => {
 | 
				
			||||||
 | 
					                self.window.window.request_redraw();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            _ => (),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        let window = &self.window;
 | 
				
			||||||
 | 
					        let Some(imgui) = self.imgui.as_mut() else {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        let mut context = imgui.context.lock().unwrap();
 | 
				
			||||||
 | 
					        imgui
 | 
				
			||||||
 | 
					            .platform
 | 
				
			||||||
 | 
					            .handle_event(context.io_mut(), &window.window, event);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn compute_game_bounds(
 | 
				
			||||||
 | 
					    window_width: f32,
 | 
				
			||||||
 | 
					    window_height: f32,
 | 
				
			||||||
 | 
					    menu_height: f32,
 | 
				
			||||||
 | 
					) -> ((f32, f32), (f32, f32)) {
 | 
				
			||||||
 | 
					    let available_width = window_width;
 | 
				
			||||||
 | 
					    let available_height = window_height - menu_height;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let width = available_width.min(available_height * 384.0 / 224.0);
 | 
				
			||||||
 | 
					    let height = available_height.min(available_width * 224.0 / 384.0);
 | 
				
			||||||
 | 
					    let x = (available_width - width) / 2.0;
 | 
				
			||||||
 | 
					    let y = menu_height + (available_height - height) / 2.0;
 | 
				
			||||||
 | 
					    ((x, y), (width, height))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
 | 
				
			||||||
 | 
					#[repr(C)]
 | 
				
			||||||
 | 
					struct Colors {
 | 
				
			||||||
 | 
					    left: [f32; 4],
 | 
				
			||||||
 | 
					    right: [f32; 4],
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,223 @@
 | 
				
			||||||
 | 
					use std::{
 | 
				
			||||||
 | 
					    sync::{Arc, RwLock},
 | 
				
			||||||
 | 
					    time::Instant,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use winit::{
 | 
				
			||||||
 | 
					    dpi::LogicalSize,
 | 
				
			||||||
 | 
					    event::{Event, KeyEvent, WindowEvent},
 | 
				
			||||||
 | 
					    event_loop::{ActiveEventLoop, EventLoopProxy},
 | 
				
			||||||
 | 
					    platform::modifier_supplement::KeyEventExtModifierSupplement,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{input::InputMapper, shrooms_vb_core::VBKey};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::{
 | 
				
			||||||
 | 
					    common::{ImguiState, UiExt, WindowState, WindowStateBuilder},
 | 
				
			||||||
 | 
					    AppWindow, UserEvent,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct InputWindow {
 | 
				
			||||||
 | 
					    window: WindowState,
 | 
				
			||||||
 | 
					    imgui: Option<ImguiState>,
 | 
				
			||||||
 | 
					    input_mapper: Arc<RwLock<InputMapper>>,
 | 
				
			||||||
 | 
					    proxy: EventLoopProxy<UserEvent>,
 | 
				
			||||||
 | 
					    now_binding: Option<VBKey>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const KEY_NAMES: [(VBKey, &str); 14] = [
 | 
				
			||||||
 | 
					    (VBKey::LU, "Up"),
 | 
				
			||||||
 | 
					    (VBKey::LD, "Down"),
 | 
				
			||||||
 | 
					    (VBKey::LL, "Left"),
 | 
				
			||||||
 | 
					    (VBKey::LR, "Right"),
 | 
				
			||||||
 | 
					    (VBKey::SEL, "Select"),
 | 
				
			||||||
 | 
					    (VBKey::STA, "Start"),
 | 
				
			||||||
 | 
					    (VBKey::B, "B"),
 | 
				
			||||||
 | 
					    (VBKey::A, "A"),
 | 
				
			||||||
 | 
					    (VBKey::LT, "L-Trigger"),
 | 
				
			||||||
 | 
					    (VBKey::RT, "R-Trigger"),
 | 
				
			||||||
 | 
					    (VBKey::RU, "R-Up"),
 | 
				
			||||||
 | 
					    (VBKey::RD, "R-Down"),
 | 
				
			||||||
 | 
					    (VBKey::RL, "R-Left"),
 | 
				
			||||||
 | 
					    (VBKey::RR, "R-Right"),
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl InputWindow {
 | 
				
			||||||
 | 
					    pub fn new(
 | 
				
			||||||
 | 
					        event_loop: &ActiveEventLoop,
 | 
				
			||||||
 | 
					        input_mapper: Arc<RwLock<InputMapper>>,
 | 
				
			||||||
 | 
					        proxy: EventLoopProxy<UserEvent>,
 | 
				
			||||||
 | 
					    ) -> Self {
 | 
				
			||||||
 | 
					        let window = WindowStateBuilder::new(event_loop)
 | 
				
			||||||
 | 
					            .with_title("Bind Inputs")
 | 
				
			||||||
 | 
					            .with_inner_size(LogicalSize::new(600, 400))
 | 
				
			||||||
 | 
					            .build();
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            window,
 | 
				
			||||||
 | 
					            imgui: None,
 | 
				
			||||||
 | 
					            input_mapper,
 | 
				
			||||||
 | 
					            now_binding: None,
 | 
				
			||||||
 | 
					            proxy,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn draw(&mut self) {
 | 
				
			||||||
 | 
					        let window = &mut self.window;
 | 
				
			||||||
 | 
					        let imgui = self.imgui.as_mut().unwrap();
 | 
				
			||||||
 | 
					        let mut context = imgui.context.lock().unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let now = Instant::now();
 | 
				
			||||||
 | 
					        context.io_mut().update_delta_time(now - imgui.last_frame);
 | 
				
			||||||
 | 
					        imgui.last_frame = now;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let frame = match window.surface.get_current_texture() {
 | 
				
			||||||
 | 
					            Ok(frame) => frame,
 | 
				
			||||||
 | 
					            Err(e) => {
 | 
				
			||||||
 | 
					                if !self.window.minimized {
 | 
				
			||||||
 | 
					                    eprintln!("dropped frame: {e:?}");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        imgui
 | 
				
			||||||
 | 
					            .platform
 | 
				
			||||||
 | 
					            .prepare_frame(context.io_mut(), &window.window)
 | 
				
			||||||
 | 
					            .expect("Failed to prepare frame");
 | 
				
			||||||
 | 
					        let ui = context.new_frame();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let mut render_key_bindings = || {
 | 
				
			||||||
 | 
					            if let Some(table) = ui.begin_table("controls", 2) {
 | 
				
			||||||
 | 
					                let binding_names = {
 | 
				
			||||||
 | 
					                    let mapper = self.input_mapper.read().unwrap();
 | 
				
			||||||
 | 
					                    mapper.binding_names()
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                ui.table_next_row();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                for (key, name) in KEY_NAMES {
 | 
				
			||||||
 | 
					                    let binding = binding_names.get(&key).map(|s| s.as_str());
 | 
				
			||||||
 | 
					                    ui.table_next_column();
 | 
				
			||||||
 | 
					                    let [space, _] = ui.content_region_avail();
 | 
				
			||||||
 | 
					                    ui.group(|| {
 | 
				
			||||||
 | 
					                        ui.right_align_text(name, space * 0.20);
 | 
				
			||||||
 | 
					                        ui.same_line();
 | 
				
			||||||
 | 
					                        let label_text = if self.now_binding == Some(key) {
 | 
				
			||||||
 | 
					                            "Press any input"
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            binding.unwrap_or("")
 | 
				
			||||||
 | 
					                        };
 | 
				
			||||||
 | 
					                        let label = format!("{}##{}", label_text, name);
 | 
				
			||||||
 | 
					                        if ui.button_with_size(label, [space * 0.60, 0.0]) {
 | 
				
			||||||
 | 
					                            self.now_binding = Some(key);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                    ui.same_line();
 | 
				
			||||||
 | 
					                    if ui.button(format!("Clear##{name}")) {
 | 
				
			||||||
 | 
					                        let mut mapper = self.input_mapper.write().unwrap();
 | 
				
			||||||
 | 
					                        mapper.clear_binding(key);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                table.end();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Some(window) = ui.fullscreen_window() {
 | 
				
			||||||
 | 
					            if let Some(tabs) = ui.tab_bar("tabs") {
 | 
				
			||||||
 | 
					                if let Some(tab) = ui.tab_item("Player 1") {
 | 
				
			||||||
 | 
					                    render_key_bindings();
 | 
				
			||||||
 | 
					                    tab.end();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                tabs.end();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            window.end();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        let mut encoder: wgpu::CommandEncoder = window
 | 
				
			||||||
 | 
					            .device
 | 
				
			||||||
 | 
					            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if imgui.last_cursor != ui.mouse_cursor() {
 | 
				
			||||||
 | 
					            imgui.last_cursor = ui.mouse_cursor();
 | 
				
			||||||
 | 
					            imgui.platform.prepare_render(ui, &window.window);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let view = frame
 | 
				
			||||||
 | 
					            .texture
 | 
				
			||||||
 | 
					            .create_view(&wgpu::TextureViewDescriptor::default());
 | 
				
			||||||
 | 
					        let mut rpass = 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(imgui.clear_color),
 | 
				
			||||||
 | 
					                    store: wgpu::StoreOp::Store,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            })],
 | 
				
			||||||
 | 
					            depth_stencil_attachment: None,
 | 
				
			||||||
 | 
					            timestamp_writes: None,
 | 
				
			||||||
 | 
					            occlusion_query_set: None,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Draw the game
 | 
				
			||||||
 | 
					        imgui
 | 
				
			||||||
 | 
					            .renderer
 | 
				
			||||||
 | 
					            .render(context.render(), &window.queue, &window.device, &mut rpass)
 | 
				
			||||||
 | 
					            .expect("Rendering failed");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        drop(rpass);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        window.queue.submit(Some(encoder.finish()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        frame.present();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn try_bind_key(&mut self, event: &KeyEvent) {
 | 
				
			||||||
 | 
					        if !event.state.is_pressed() {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        let Some(vb) = self.now_binding.take() else {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        let mut mapper = self.input_mapper.write().unwrap();
 | 
				
			||||||
 | 
					        mapper.bind_key(vb, event.key_without_modifiers());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl AppWindow for InputWindow {
 | 
				
			||||||
 | 
					    fn id(&self) -> winit::window::WindowId {
 | 
				
			||||||
 | 
					        self.window.window.id()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn init(&mut self) {
 | 
				
			||||||
 | 
					        self.imgui = Some(ImguiState::new(&self.window));
 | 
				
			||||||
 | 
					        self.window.window.request_redraw();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn handle_event(&mut self, _: &ActiveEventLoop, event: &Event<UserEvent>) {
 | 
				
			||||||
 | 
					        match event {
 | 
				
			||||||
 | 
					            Event::WindowEvent { event, .. } => match event {
 | 
				
			||||||
 | 
					                WindowEvent::Resized(size) => self.window.handle_resize(size),
 | 
				
			||||||
 | 
					                WindowEvent::CloseRequested => self
 | 
				
			||||||
 | 
					                    .proxy
 | 
				
			||||||
 | 
					                    .send_event(UserEvent::CloseWindow(self.id()))
 | 
				
			||||||
 | 
					                    .unwrap(),
 | 
				
			||||||
 | 
					                WindowEvent::KeyboardInput { event, .. } => self.try_bind_key(event),
 | 
				
			||||||
 | 
					                WindowEvent::RedrawRequested => self.draw(),
 | 
				
			||||||
 | 
					                _ => (),
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            Event::AboutToWait => {
 | 
				
			||||||
 | 
					                self.window.window.request_redraw();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            _ => (),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let window = &self.window;
 | 
				
			||||||
 | 
					        let Some(imgui) = self.imgui.as_mut() else {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        let mut context = imgui.context.lock().unwrap();
 | 
				
			||||||
 | 
					        imgui
 | 
				
			||||||
 | 
					            .platform
 | 
				
			||||||
 | 
					            .handle_event(context.io_mut(), &window.window, event);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,101 @@
 | 
				
			||||||
 | 
					use anyhow::{bail, Result};
 | 
				
			||||||
 | 
					use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
 | 
				
			||||||
 | 
					use itertools::Itertools;
 | 
				
			||||||
 | 
					use rubato::{FftFixedInOut, Resampler};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct Audio {
 | 
				
			||||||
 | 
					    #[allow(unused)]
 | 
				
			||||||
 | 
					    stream: cpal::Stream,
 | 
				
			||||||
 | 
					    sampler: FftFixedInOut<f32>,
 | 
				
			||||||
 | 
					    input_buffer: Vec<Vec<f32>>,
 | 
				
			||||||
 | 
					    output_buffer: Vec<Vec<f32>>,
 | 
				
			||||||
 | 
					    sample_sink: rtrb::Producer<f32>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Audio {
 | 
				
			||||||
 | 
					    pub fn init() -> Result<Self> {
 | 
				
			||||||
 | 
					        let host = cpal::default_host();
 | 
				
			||||||
 | 
					        let Some(device) = host.default_output_device() else {
 | 
				
			||||||
 | 
					            bail!("No output device available");
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        let Some(config) = device
 | 
				
			||||||
 | 
					            .supported_output_configs()?
 | 
				
			||||||
 | 
					            .find(|c| c.channels() == 2 && c.sample_format().is_float())
 | 
				
			||||||
 | 
					        else {
 | 
				
			||||||
 | 
					            bail!("No suitable output config available");
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        let mut config = config.with_max_sample_rate().config();
 | 
				
			||||||
 | 
					        let sampler = FftFixedInOut::new(41700, config.sample_rate.0 as usize, 834, 2)?;
 | 
				
			||||||
 | 
					        config.buffer_size = cpal::BufferSize::Fixed(sampler.output_frames_max() as u32);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let input_buffer = sampler.input_buffer_allocate(true);
 | 
				
			||||||
 | 
					        let output_buffer = sampler.output_buffer_allocate(true);
 | 
				
			||||||
 | 
					        let (sample_sink, mut sample_source) =
 | 
				
			||||||
 | 
					            rtrb::RingBuffer::new(sampler.output_frames_max() * 4);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let stream = device.build_output_stream(
 | 
				
			||||||
 | 
					            &config,
 | 
				
			||||||
 | 
					            move |data: &mut [f32], _| {
 | 
				
			||||||
 | 
					                let requested = data.len();
 | 
				
			||||||
 | 
					                let chunk = match sample_source.read_chunk(data.len()) {
 | 
				
			||||||
 | 
					                    Ok(c) => c,
 | 
				
			||||||
 | 
					                    Err(rtrb::chunks::ChunkError::TooFewSlots(n)) => {
 | 
				
			||||||
 | 
					                        sample_source.read_chunk(n).unwrap()
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                let len = chunk.len();
 | 
				
			||||||
 | 
					                let (first, second) = chunk.as_slices();
 | 
				
			||||||
 | 
					                data[0..first.len()].copy_from_slice(first);
 | 
				
			||||||
 | 
					                data[first.len()..len].copy_from_slice(second);
 | 
				
			||||||
 | 
					                for rest in &mut data[len..requested] {
 | 
				
			||||||
 | 
					                    *rest = 0.0;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                chunk.commit_all();
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            move |err| eprintln!("stream error: {err}"),
 | 
				
			||||||
 | 
					            None,
 | 
				
			||||||
 | 
					        )?;
 | 
				
			||||||
 | 
					        stream.play()?;
 | 
				
			||||||
 | 
					        Ok(Self {
 | 
				
			||||||
 | 
					            stream,
 | 
				
			||||||
 | 
					            sampler,
 | 
				
			||||||
 | 
					            input_buffer,
 | 
				
			||||||
 | 
					            output_buffer,
 | 
				
			||||||
 | 
					            sample_sink,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn update(&mut self, samples: &[f32]) {
 | 
				
			||||||
 | 
					        for sample in samples.chunks_exact(2) {
 | 
				
			||||||
 | 
					            for (channel, value) in self.input_buffer.iter_mut().zip(sample) {
 | 
				
			||||||
 | 
					                channel.push(*value);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if self.input_buffer[0].len() >= self.sampler.input_frames_next() {
 | 
				
			||||||
 | 
					                let (_, output_samples) = self
 | 
				
			||||||
 | 
					                    .sampler
 | 
				
			||||||
 | 
					                    .process_into_buffer(&self.input_buffer, &mut self.output_buffer, None)
 | 
				
			||||||
 | 
					                    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let chunk = match self.sample_sink.write_chunk_uninit(output_samples * 2) {
 | 
				
			||||||
 | 
					                    Ok(c) => c,
 | 
				
			||||||
 | 
					                    Err(rtrb::chunks::ChunkError::TooFewSlots(n)) => {
 | 
				
			||||||
 | 
					                        self.sample_sink.write_chunk_uninit(n).unwrap()
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                let interleaved = self.output_buffer[0]
 | 
				
			||||||
 | 
					                    .iter()
 | 
				
			||||||
 | 
					                    .interleave(self.output_buffer[1].iter())
 | 
				
			||||||
 | 
					                    .cloned();
 | 
				
			||||||
 | 
					                chunk.fill_from_iter(interleaved);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                for channel in &mut self.input_buffer {
 | 
				
			||||||
 | 
					                    channel.clear();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        while self.sample_sink.slots() < self.sampler.output_frames_max() * 2 {
 | 
				
			||||||
 | 
					            std::hint::spin_loop();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,50 @@
 | 
				
			||||||
 | 
					use std::sync::{Arc, RwLock};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use winit::event::{ElementState, KeyEvent};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{input::InputMapper, shrooms_vb_core::VBKey};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct ControllerState {
 | 
				
			||||||
 | 
					    input_mapper: Arc<RwLock<InputMapper>>,
 | 
				
			||||||
 | 
					    pressed: VBKey,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ControllerState {
 | 
				
			||||||
 | 
					    pub fn new(input_mapper: Arc<RwLock<InputMapper>>) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            input_mapper,
 | 
				
			||||||
 | 
					            pressed: VBKey::SGN,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn pressed(&self) -> VBKey {
 | 
				
			||||||
 | 
					        self.pressed
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn key_event(&mut self, event: &KeyEvent) -> bool {
 | 
				
			||||||
 | 
					        let Some(input) = self.key_event_to_input(event) else {
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        match event.state {
 | 
				
			||||||
 | 
					            ElementState::Pressed => {
 | 
				
			||||||
 | 
					                if self.pressed.contains(input) {
 | 
				
			||||||
 | 
					                    return false;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                self.pressed.insert(input);
 | 
				
			||||||
 | 
					                true
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            ElementState::Released => {
 | 
				
			||||||
 | 
					                if !self.pressed.contains(input) {
 | 
				
			||||||
 | 
					                    return false;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                self.pressed.remove(input);
 | 
				
			||||||
 | 
					                true
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn key_event_to_input(&self, event: &KeyEvent) -> Option<VBKey> {
 | 
				
			||||||
 | 
					        let mapper = self.input_mapper.read().unwrap();
 | 
				
			||||||
 | 
					        mapper.key_event(event)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,199 @@
 | 
				
			||||||
 | 
					use std::{
 | 
				
			||||||
 | 
					    fs,
 | 
				
			||||||
 | 
					    path::{Path, PathBuf},
 | 
				
			||||||
 | 
					    sync::{
 | 
				
			||||||
 | 
					        atomic::{AtomicBool, Ordering},
 | 
				
			||||||
 | 
					        mpsc::{self, RecvError, TryRecvError},
 | 
				
			||||||
 | 
					        Arc,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use anyhow::Result;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    audio::Audio,
 | 
				
			||||||
 | 
					    renderer::GameRenderer,
 | 
				
			||||||
 | 
					    shrooms_vb_core::{CoreVB, VBKey},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct EmulatorBuilder {
 | 
				
			||||||
 | 
					    rom: Option<PathBuf>,
 | 
				
			||||||
 | 
					    commands: mpsc::Receiver<EmulatorCommand>,
 | 
				
			||||||
 | 
					    running: Arc<AtomicBool>,
 | 
				
			||||||
 | 
					    has_game: Arc<AtomicBool>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl EmulatorBuilder {
 | 
				
			||||||
 | 
					    pub fn new() -> (Self, EmulatorClient) {
 | 
				
			||||||
 | 
					        let (queue, commands) = mpsc::channel();
 | 
				
			||||||
 | 
					        let builder = Self {
 | 
				
			||||||
 | 
					            rom: None,
 | 
				
			||||||
 | 
					            commands,
 | 
				
			||||||
 | 
					            running: Arc::new(AtomicBool::new(false)),
 | 
				
			||||||
 | 
					            has_game: Arc::new(AtomicBool::new(false)),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        let client = EmulatorClient {
 | 
				
			||||||
 | 
					            queue,
 | 
				
			||||||
 | 
					            running: builder.running.clone(),
 | 
				
			||||||
 | 
					            has_game: builder.has_game.clone(),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        (builder, client)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn with_rom(self, path: &Path) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            rom: Some(path.into()),
 | 
				
			||||||
 | 
					            ..self
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn build(self) -> Result<Emulator> {
 | 
				
			||||||
 | 
					        let mut emulator = Emulator::new(self.commands, self.running, self.has_game)?;
 | 
				
			||||||
 | 
					        if let Some(path) = self.rom {
 | 
				
			||||||
 | 
					            emulator.load_rom(&path)?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        Ok(emulator)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct Emulator {
 | 
				
			||||||
 | 
					    sim: CoreVB,
 | 
				
			||||||
 | 
					    audio: Audio,
 | 
				
			||||||
 | 
					    commands: mpsc::Receiver<EmulatorCommand>,
 | 
				
			||||||
 | 
					    renderer: Option<GameRenderer>,
 | 
				
			||||||
 | 
					    running: Arc<AtomicBool>,
 | 
				
			||||||
 | 
					    has_game: Arc<AtomicBool>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Emulator {
 | 
				
			||||||
 | 
					    fn new(
 | 
				
			||||||
 | 
					        commands: mpsc::Receiver<EmulatorCommand>,
 | 
				
			||||||
 | 
					        running: Arc<AtomicBool>,
 | 
				
			||||||
 | 
					        has_game: Arc<AtomicBool>,
 | 
				
			||||||
 | 
					    ) -> Result<Self> {
 | 
				
			||||||
 | 
					        Ok(Self {
 | 
				
			||||||
 | 
					            sim: CoreVB::new(),
 | 
				
			||||||
 | 
					            audio: Audio::init()?,
 | 
				
			||||||
 | 
					            commands,
 | 
				
			||||||
 | 
					            renderer: None,
 | 
				
			||||||
 | 
					            running,
 | 
				
			||||||
 | 
					            has_game,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn load_rom(&mut self, path: &Path) -> Result<()> {
 | 
				
			||||||
 | 
					        let bytes = fs::read(path)?;
 | 
				
			||||||
 | 
					        self.sim.reset();
 | 
				
			||||||
 | 
					        self.sim.load_rom(bytes)?;
 | 
				
			||||||
 | 
					        self.has_game.store(true, Ordering::Release);
 | 
				
			||||||
 | 
					        self.running.store(true, Ordering::Release);
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn run(&mut self) {
 | 
				
			||||||
 | 
					        let mut eye_contents = vec![0u8; 384 * 224 * 2];
 | 
				
			||||||
 | 
					        let mut audio_samples = vec![];
 | 
				
			||||||
 | 
					        loop {
 | 
				
			||||||
 | 
					            let mut idle = true;
 | 
				
			||||||
 | 
					            if self.running.load(Ordering::Acquire) {
 | 
				
			||||||
 | 
					                idle = false;
 | 
				
			||||||
 | 
					                self.sim.emulate_frame();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if let Some(renderer) = &mut self.renderer {
 | 
				
			||||||
 | 
					                if self.sim.read_pixels(&mut eye_contents) {
 | 
				
			||||||
 | 
					                    idle = false;
 | 
				
			||||||
 | 
					                    renderer.render(&eye_contents);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            self.sim.read_samples(&mut audio_samples);
 | 
				
			||||||
 | 
					            if !audio_samples.is_empty() {
 | 
				
			||||||
 | 
					                idle = false;
 | 
				
			||||||
 | 
					                self.audio.update(&audio_samples);
 | 
				
			||||||
 | 
					                audio_samples.clear();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if idle {
 | 
				
			||||||
 | 
					                // The game is paused, and we have output all the video/audio we have.
 | 
				
			||||||
 | 
					                // Block the thread until a new command comes in.
 | 
				
			||||||
 | 
					                match self.commands.recv() {
 | 
				
			||||||
 | 
					                    Ok(command) => self.handle_command(command),
 | 
				
			||||||
 | 
					                    Err(RecvError) => {
 | 
				
			||||||
 | 
					                        return;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            loop {
 | 
				
			||||||
 | 
					                match self.commands.try_recv() {
 | 
				
			||||||
 | 
					                    Ok(command) => self.handle_command(command),
 | 
				
			||||||
 | 
					                    Err(TryRecvError::Empty) => {
 | 
				
			||||||
 | 
					                        break;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    Err(TryRecvError::Disconnected) => {
 | 
				
			||||||
 | 
					                        return;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn handle_command(&mut self, command: EmulatorCommand) {
 | 
				
			||||||
 | 
					        match command {
 | 
				
			||||||
 | 
					            EmulatorCommand::SetRenderer(renderer) => {
 | 
				
			||||||
 | 
					                self.renderer = Some(renderer);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            EmulatorCommand::LoadGame(path) => {
 | 
				
			||||||
 | 
					                if let Err(error) = self.load_rom(&path) {
 | 
				
			||||||
 | 
					                    eprintln!("error loading rom: {}", error);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            EmulatorCommand::Pause => {
 | 
				
			||||||
 | 
					                self.running.store(false, Ordering::Release);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            EmulatorCommand::Resume => {
 | 
				
			||||||
 | 
					                if self.has_game.load(Ordering::Acquire) {
 | 
				
			||||||
 | 
					                    self.running.store(true, Ordering::Relaxed);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            EmulatorCommand::Reset => {
 | 
				
			||||||
 | 
					                self.sim.reset();
 | 
				
			||||||
 | 
					                self.running.store(true, Ordering::Release);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            EmulatorCommand::SetKeys(keys) => {
 | 
				
			||||||
 | 
					                self.sim.set_keys(keys);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					pub enum EmulatorCommand {
 | 
				
			||||||
 | 
					    SetRenderer(GameRenderer),
 | 
				
			||||||
 | 
					    LoadGame(PathBuf),
 | 
				
			||||||
 | 
					    Pause,
 | 
				
			||||||
 | 
					    Resume,
 | 
				
			||||||
 | 
					    Reset,
 | 
				
			||||||
 | 
					    SetKeys(VBKey),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone)]
 | 
				
			||||||
 | 
					pub struct EmulatorClient {
 | 
				
			||||||
 | 
					    queue: mpsc::Sender<EmulatorCommand>,
 | 
				
			||||||
 | 
					    running: Arc<AtomicBool>,
 | 
				
			||||||
 | 
					    has_game: Arc<AtomicBool>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl EmulatorClient {
 | 
				
			||||||
 | 
					    pub fn is_running(&self) -> bool {
 | 
				
			||||||
 | 
					        self.running.load(Ordering::Acquire)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    pub fn has_game(&self) -> bool {
 | 
				
			||||||
 | 
					        self.has_game.load(Ordering::Acquire)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    pub fn send_command(&self, command: EmulatorCommand) {
 | 
				
			||||||
 | 
					        if let Err(err) = self.queue.send(command) {
 | 
				
			||||||
 | 
					            eprintln!(
 | 
				
			||||||
 | 
					                "could not send command {:?} as emulator is shut down",
 | 
				
			||||||
 | 
					                err.0
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,71 @@
 | 
				
			||||||
 | 
					use std::collections::HashMap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use winit::{
 | 
				
			||||||
 | 
					    event::KeyEvent,
 | 
				
			||||||
 | 
					    keyboard::{Key, NamedKey},
 | 
				
			||||||
 | 
					    platform::modifier_supplement::KeyEventExtModifierSupplement,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::shrooms_vb_core::VBKey;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct InputMapper {
 | 
				
			||||||
 | 
					    vb_bindings: HashMap<VBKey, Key>,
 | 
				
			||||||
 | 
					    key_bindings: HashMap<Key, VBKey>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl InputMapper {
 | 
				
			||||||
 | 
					    pub fn new() -> Self {
 | 
				
			||||||
 | 
					        let mut mapper = Self {
 | 
				
			||||||
 | 
					            vb_bindings: HashMap::new(),
 | 
				
			||||||
 | 
					            key_bindings: HashMap::new(),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        mapper.bind_key(VBKey::SEL, Key::Character("a".into()));
 | 
				
			||||||
 | 
					        mapper.bind_key(VBKey::STA, Key::Character("s".into()));
 | 
				
			||||||
 | 
					        mapper.bind_key(VBKey::B, Key::Character("d".into()));
 | 
				
			||||||
 | 
					        mapper.bind_key(VBKey::A, Key::Character("f".into()));
 | 
				
			||||||
 | 
					        mapper.bind_key(VBKey::LT, Key::Character("e".into()));
 | 
				
			||||||
 | 
					        mapper.bind_key(VBKey::RT, Key::Character("r".into()));
 | 
				
			||||||
 | 
					        mapper.bind_key(VBKey::RU, Key::Character("i".into()));
 | 
				
			||||||
 | 
					        mapper.bind_key(VBKey::RL, Key::Character("j".into()));
 | 
				
			||||||
 | 
					        mapper.bind_key(VBKey::RD, Key::Character("k".into()));
 | 
				
			||||||
 | 
					        mapper.bind_key(VBKey::RR, Key::Character("l".into()));
 | 
				
			||||||
 | 
					        mapper.bind_key(VBKey::LU, Key::Named(NamedKey::ArrowUp));
 | 
				
			||||||
 | 
					        mapper.bind_key(VBKey::LL, Key::Named(NamedKey::ArrowLeft));
 | 
				
			||||||
 | 
					        mapper.bind_key(VBKey::LD, Key::Named(NamedKey::ArrowDown));
 | 
				
			||||||
 | 
					        mapper.bind_key(VBKey::LR, Key::Named(NamedKey::ArrowRight));
 | 
				
			||||||
 | 
					        mapper
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn binding_names(&self) -> HashMap<VBKey, String> {
 | 
				
			||||||
 | 
					        self.vb_bindings
 | 
				
			||||||
 | 
					            .iter()
 | 
				
			||||||
 | 
					            .map(|(k, v)| {
 | 
				
			||||||
 | 
					                let name = match v {
 | 
				
			||||||
 | 
					                    Key::Character(char) => char.to_string(),
 | 
				
			||||||
 | 
					                    Key::Named(key) => format!("{:?}", key),
 | 
				
			||||||
 | 
					                    k => format!("{:?}", k),
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                (*k, name)
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .collect()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn bind_key(&mut self, vb: VBKey, key: Key) {
 | 
				
			||||||
 | 
					        if let Some(old) = self.vb_bindings.insert(vb, key.clone()) {
 | 
				
			||||||
 | 
					            self.key_bindings.remove(&old);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        self.key_bindings.insert(key, vb);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn clear_binding(&mut self, vb: VBKey) {
 | 
				
			||||||
 | 
					        if let Some(old) = self.vb_bindings.remove(&vb) {
 | 
				
			||||||
 | 
					            self.key_bindings.remove(&old);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn key_event(&self, event: &KeyEvent) -> Option<VBKey> {
 | 
				
			||||||
 | 
					        self.key_bindings
 | 
				
			||||||
 | 
					            .get(&event.key_without_modifiers())
 | 
				
			||||||
 | 
					            .cloned()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,50 @@
 | 
				
			||||||
 | 
					use std::{path::PathBuf, process};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use anyhow::Result;
 | 
				
			||||||
 | 
					use app::App;
 | 
				
			||||||
 | 
					use clap::Parser;
 | 
				
			||||||
 | 
					use emulator::EmulatorBuilder;
 | 
				
			||||||
 | 
					use thread_priority::{ThreadBuilder, ThreadPriority};
 | 
				
			||||||
 | 
					use winit::event_loop::{ControlFlow, EventLoop};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					mod app;
 | 
				
			||||||
 | 
					mod audio;
 | 
				
			||||||
 | 
					mod controller;
 | 
				
			||||||
 | 
					mod emulator;
 | 
				
			||||||
 | 
					mod input;
 | 
				
			||||||
 | 
					mod renderer;
 | 
				
			||||||
 | 
					mod shrooms_vb_core;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Parser)]
 | 
				
			||||||
 | 
					struct Args {
 | 
				
			||||||
 | 
					    rom: Option<PathBuf>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn main() -> Result<()> {
 | 
				
			||||||
 | 
					    let args = Args::parse();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let (mut builder, client) = EmulatorBuilder::new();
 | 
				
			||||||
 | 
					    if let Some(path) = args.rom {
 | 
				
			||||||
 | 
					        builder = builder.with_rom(&path);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ThreadBuilder::default()
 | 
				
			||||||
 | 
					        .name("Emulator".to_owned())
 | 
				
			||||||
 | 
					        .priority(ThreadPriority::Max)
 | 
				
			||||||
 | 
					        .spawn_careless(move || {
 | 
				
			||||||
 | 
					            let mut emulator = match builder.build() {
 | 
				
			||||||
 | 
					                Ok(e) => e,
 | 
				
			||||||
 | 
					                Err(err) => {
 | 
				
			||||||
 | 
					                    eprintln!("Error initializing emulator: {err}");
 | 
				
			||||||
 | 
					                    process::exit(1);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            emulator.run();
 | 
				
			||||||
 | 
					        })?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let event_loop = EventLoop::with_user_event().build().unwrap();
 | 
				
			||||||
 | 
					    event_loop.set_control_flow(ControlFlow::Poll);
 | 
				
			||||||
 | 
					    let proxy = event_loop.create_proxy();
 | 
				
			||||||
 | 
					    event_loop.run_app(&mut App::new(client, proxy))?;
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,53 @@
 | 
				
			||||||
 | 
					use std::sync::Arc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use wgpu::{
 | 
				
			||||||
 | 
					    Extent3d, ImageCopyTexture, ImageDataLayout, Origin3d, Queue, Texture, TextureDescriptor,
 | 
				
			||||||
 | 
					    TextureFormat, TextureUsages,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					pub struct GameRenderer {
 | 
				
			||||||
 | 
					    pub queue: Arc<Queue>,
 | 
				
			||||||
 | 
					    pub eyes: Arc<Texture>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl GameRenderer {
 | 
				
			||||||
 | 
					    pub fn render(&self, buffer: &[u8]) {
 | 
				
			||||||
 | 
					        let texture = ImageCopyTexture {
 | 
				
			||||||
 | 
					            texture: &self.eyes,
 | 
				
			||||||
 | 
					            mip_level: 0,
 | 
				
			||||||
 | 
					            origin: Origin3d::ZERO,
 | 
				
			||||||
 | 
					            aspect: wgpu::TextureAspect::All,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        let size = Extent3d {
 | 
				
			||||||
 | 
					            width: 384,
 | 
				
			||||||
 | 
					            height: 224,
 | 
				
			||||||
 | 
					            depth_or_array_layers: 1,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        let data_layout = ImageDataLayout {
 | 
				
			||||||
 | 
					            offset: 0,
 | 
				
			||||||
 | 
					            bytes_per_row: Some(384 * 2),
 | 
				
			||||||
 | 
					            rows_per_image: Some(224),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        self.queue.write_texture(texture, buffer, data_layout, size);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    pub fn create_texture(device: &wgpu::Device, name: &str) -> Texture {
 | 
				
			||||||
 | 
					        let desc = TextureDescriptor {
 | 
				
			||||||
 | 
					            label: Some(name),
 | 
				
			||||||
 | 
					            size: Extent3d {
 | 
				
			||||||
 | 
					                width: 384,
 | 
				
			||||||
 | 
					                height: 224,
 | 
				
			||||||
 | 
					                depth_or_array_layers: 1,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            mip_level_count: 1,
 | 
				
			||||||
 | 
					            sample_count: 1,
 | 
				
			||||||
 | 
					            dimension: wgpu::TextureDimension::D2,
 | 
				
			||||||
 | 
					            format: TextureFormat::Rg8Unorm,
 | 
				
			||||||
 | 
					            usage: TextureUsages::COPY_SRC
 | 
				
			||||||
 | 
					                | TextureUsages::COPY_DST
 | 
				
			||||||
 | 
					                | TextureUsages::TEXTURE_BINDING,
 | 
				
			||||||
 | 
					            view_formats: &[TextureFormat::Rg8Unorm],
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        device.create_texture(&desc)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,251 @@
 | 
				
			||||||
 | 
					use std::{ffi::c_void, ptr, slice};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use anyhow::{anyhow, Result};
 | 
				
			||||||
 | 
					use bitflags::bitflags;
 | 
				
			||||||
 | 
					use num_derive::{FromPrimitive, ToPrimitive};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[repr(C)]
 | 
				
			||||||
 | 
					struct VB {
 | 
				
			||||||
 | 
					    _data: [u8; 0],
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[allow(non_camel_case_types)]
 | 
				
			||||||
 | 
					type c_int = i32;
 | 
				
			||||||
 | 
					#[allow(non_camel_case_types)]
 | 
				
			||||||
 | 
					type c_uint = u32;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[repr(u32)]
 | 
				
			||||||
 | 
					#[derive(FromPrimitive, ToPrimitive)]
 | 
				
			||||||
 | 
					enum VBDataType {
 | 
				
			||||||
 | 
					    S8 = 0,
 | 
				
			||||||
 | 
					    U8 = 1,
 | 
				
			||||||
 | 
					    S16 = 2,
 | 
				
			||||||
 | 
					    U16 = 3,
 | 
				
			||||||
 | 
					    S32 = 4,
 | 
				
			||||||
 | 
					    F32 = 5,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bitflags! {
 | 
				
			||||||
 | 
					    #[repr(transparent)]
 | 
				
			||||||
 | 
					    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
 | 
				
			||||||
 | 
					    pub struct VBKey: u16 {
 | 
				
			||||||
 | 
					        const PWR = 0x0001;
 | 
				
			||||||
 | 
					        const SGN = 0x0002;
 | 
				
			||||||
 | 
					        const A   = 0x0004;
 | 
				
			||||||
 | 
					        const B   = 0x0008;
 | 
				
			||||||
 | 
					        const RT  = 0x0010;
 | 
				
			||||||
 | 
					        const LT  = 0x0020;
 | 
				
			||||||
 | 
					        const RU  = 0x0040;
 | 
				
			||||||
 | 
					        const RR  = 0x0080;
 | 
				
			||||||
 | 
					        const LR  = 0x0100;
 | 
				
			||||||
 | 
					        const LL  = 0x0200;
 | 
				
			||||||
 | 
					        const LD  = 0x0400;
 | 
				
			||||||
 | 
					        const LU  = 0x0800;
 | 
				
			||||||
 | 
					        const STA = 0x1000;
 | 
				
			||||||
 | 
					        const SEL = 0x2000;
 | 
				
			||||||
 | 
					        const RL  = 0x4000;
 | 
				
			||||||
 | 
					        const RD  = 0x8000;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type OnFrame = extern "C" fn(sim: *mut VB) -> c_int;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[link(name = "vb")]
 | 
				
			||||||
 | 
					extern "C" {
 | 
				
			||||||
 | 
					    #[link_name = "vbEmulate"]
 | 
				
			||||||
 | 
					    fn vb_emulate(sim: *mut VB, cycles: *mut u32) -> c_int;
 | 
				
			||||||
 | 
					    #[link_name = "vbGetCartROM"]
 | 
				
			||||||
 | 
					    fn vb_get_cart_rom(sim: *mut VB, size: *mut u32) -> *mut c_void;
 | 
				
			||||||
 | 
					    #[link_name = "vbGetPixels"]
 | 
				
			||||||
 | 
					    fn vb_get_pixels(
 | 
				
			||||||
 | 
					        sim: *mut VB,
 | 
				
			||||||
 | 
					        left: *mut c_void,
 | 
				
			||||||
 | 
					        left_stride_x: c_int,
 | 
				
			||||||
 | 
					        left_stride_y: c_int,
 | 
				
			||||||
 | 
					        right: *mut c_void,
 | 
				
			||||||
 | 
					        right_stride_x: c_int,
 | 
				
			||||||
 | 
					        right_stride_y: c_int,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    #[link_name = "vbGetSamples"]
 | 
				
			||||||
 | 
					    fn vb_get_samples(
 | 
				
			||||||
 | 
					        sim: *mut VB,
 | 
				
			||||||
 | 
					        typ_: *mut VBDataType,
 | 
				
			||||||
 | 
					        capacity: *mut c_uint,
 | 
				
			||||||
 | 
					        position: *mut c_uint,
 | 
				
			||||||
 | 
					    ) -> *mut c_void;
 | 
				
			||||||
 | 
					    #[link_name = "vbGetUserData"]
 | 
				
			||||||
 | 
					    fn vb_get_user_data(sim: *mut VB) -> *mut c_void;
 | 
				
			||||||
 | 
					    #[link_name = "vbInit"]
 | 
				
			||||||
 | 
					    fn vb_init(sim: *mut VB) -> *mut VB;
 | 
				
			||||||
 | 
					    #[link_name = "vbReset"]
 | 
				
			||||||
 | 
					    fn vb_reset(sim: *mut VB);
 | 
				
			||||||
 | 
					    #[link_name = "vbSetCartROM"]
 | 
				
			||||||
 | 
					    fn vb_set_cart_rom(sim: *mut VB, rom: *mut c_void, size: u32) -> c_int;
 | 
				
			||||||
 | 
					    #[link_name = "vbSetKeys"]
 | 
				
			||||||
 | 
					    fn vb_set_keys(sim: *mut VB, keys: u16) -> u16;
 | 
				
			||||||
 | 
					    #[link_name = "vbSetFrameCallback"]
 | 
				
			||||||
 | 
					    fn vb_set_frame_callback(sim: *mut VB, on_frame: OnFrame);
 | 
				
			||||||
 | 
					    #[link_name = "vbSetSamples"]
 | 
				
			||||||
 | 
					    fn vb_set_samples(
 | 
				
			||||||
 | 
					        sim: *mut VB,
 | 
				
			||||||
 | 
					        samples: *mut c_void,
 | 
				
			||||||
 | 
					        typ_: VBDataType,
 | 
				
			||||||
 | 
					        capacity: c_uint,
 | 
				
			||||||
 | 
					    ) -> c_int;
 | 
				
			||||||
 | 
					    #[link_name = "vbSetUserData"]
 | 
				
			||||||
 | 
					    fn vb_set_user_data(sim: *mut VB, tag: *mut c_void);
 | 
				
			||||||
 | 
					    #[link_name = "vbSizeOf"]
 | 
				
			||||||
 | 
					    fn vb_size_of() -> usize;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					extern "C" fn on_frame(sim: *mut VB) -> i32 {
 | 
				
			||||||
 | 
					    // SAFETY: the *mut VB owns its userdata.
 | 
				
			||||||
 | 
					    // There is no way for the userdata to be null or otherwise invalid.
 | 
				
			||||||
 | 
					    let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
 | 
				
			||||||
 | 
					    data.frame_seen = true;
 | 
				
			||||||
 | 
					    1
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const AUDIO_CAPACITY_SAMPLES: usize = 834 * 4;
 | 
				
			||||||
 | 
					const AUDIO_CAPACITY_FLOATS: usize = AUDIO_CAPACITY_SAMPLES * 2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct VBState {
 | 
				
			||||||
 | 
					    frame_seen: bool,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct CoreVB {
 | 
				
			||||||
 | 
					    sim: *mut VB,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SAFETY: the memory pointed to by sim is valid
 | 
				
			||||||
 | 
					unsafe impl Send for CoreVB {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl CoreVB {
 | 
				
			||||||
 | 
					    pub fn new() -> Self {
 | 
				
			||||||
 | 
					        // init the VB instance itself
 | 
				
			||||||
 | 
					        let size = unsafe { vb_size_of() };
 | 
				
			||||||
 | 
					        // allocate a vec of u64 so that this memory is 8-byte aligned
 | 
				
			||||||
 | 
					        let memory = vec![0u64; size.div_ceil(4)];
 | 
				
			||||||
 | 
					        let sim: *mut VB = Box::into_raw(memory.into_boxed_slice()).cast();
 | 
				
			||||||
 | 
					        unsafe { vb_init(sim) };
 | 
				
			||||||
 | 
					        unsafe { vb_reset(sim) };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // set up userdata
 | 
				
			||||||
 | 
					        let state = VBState { frame_seen: false };
 | 
				
			||||||
 | 
					        unsafe { vb_set_user_data(sim, Box::into_raw(Box::new(state)).cast()) };
 | 
				
			||||||
 | 
					        unsafe { vb_set_frame_callback(sim, on_frame) };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // set up audio buffer
 | 
				
			||||||
 | 
					        let audio_buffer = vec![0.0f32; AUDIO_CAPACITY_FLOATS];
 | 
				
			||||||
 | 
					        let samples: *mut c_void = Box::into_raw(audio_buffer.into_boxed_slice()).cast();
 | 
				
			||||||
 | 
					        unsafe { vb_set_samples(sim, samples, VBDataType::F32, AUDIO_CAPACITY_SAMPLES as u32) };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        CoreVB { sim }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn reset(&mut self) {
 | 
				
			||||||
 | 
					        unsafe { vb_reset(self.sim) };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn load_rom(&mut self, rom: Vec<u8>) -> Result<()> {
 | 
				
			||||||
 | 
					        self.unload_rom();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let size = rom.len() as u32;
 | 
				
			||||||
 | 
					        let rom = Box::into_raw(rom.into_boxed_slice()).cast();
 | 
				
			||||||
 | 
					        let status = unsafe { vb_set_cart_rom(self.sim, rom, size) };
 | 
				
			||||||
 | 
					        if status == 0 {
 | 
				
			||||||
 | 
					            Ok(())
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            let _: Vec<u8> =
 | 
				
			||||||
 | 
					                unsafe { Vec::from_raw_parts(rom.cast(), size as usize, size as usize) };
 | 
				
			||||||
 | 
					            Err(anyhow!("Invalid ROM size of {} bytes", size))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn unload_rom(&mut self) -> Option<Vec<u8>> {
 | 
				
			||||||
 | 
					        let mut size = 0;
 | 
				
			||||||
 | 
					        let rom = unsafe { vb_get_cart_rom(self.sim, &mut size) };
 | 
				
			||||||
 | 
					        if rom.is_null() {
 | 
				
			||||||
 | 
					            return None;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        unsafe { vb_set_cart_rom(self.sim, ptr::null_mut(), 0) };
 | 
				
			||||||
 | 
					        let vec = unsafe { Vec::from_raw_parts(rom.cast(), size as usize, size as usize) };
 | 
				
			||||||
 | 
					        Some(vec)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn emulate_frame(&mut self) {
 | 
				
			||||||
 | 
					        let mut cycles = 20_000_000;
 | 
				
			||||||
 | 
					        unsafe { vb_emulate(self.sim, &mut cycles) };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn read_pixels(&mut self, buffers: &mut [u8]) -> bool {
 | 
				
			||||||
 | 
					        // SAFETY: the *mut VB owns its userdata.
 | 
				
			||||||
 | 
					        // There is no way for the userdata to be null or otherwise invalid.
 | 
				
			||||||
 | 
					        let data: &mut VBState = unsafe { &mut *vb_get_user_data(self.sim).cast() };
 | 
				
			||||||
 | 
					        if !data.frame_seen {
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        data.frame_seen = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // the buffer must be big enough for our data
 | 
				
			||||||
 | 
					        assert!(buffers.len() >= 384 * 224 * 2);
 | 
				
			||||||
 | 
					        unsafe {
 | 
				
			||||||
 | 
					            vb_get_pixels(
 | 
				
			||||||
 | 
					                self.sim,
 | 
				
			||||||
 | 
					                buffers.as_mut_ptr().cast(),
 | 
				
			||||||
 | 
					                2,
 | 
				
			||||||
 | 
					                384 * 2,
 | 
				
			||||||
 | 
					                buffers.as_mut_ptr().offset(1).cast(),
 | 
				
			||||||
 | 
					                2,
 | 
				
			||||||
 | 
					                384 * 2,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn read_samples(&mut self, samples: &mut Vec<f32>) {
 | 
				
			||||||
 | 
					        let mut position = 0;
 | 
				
			||||||
 | 
					        let ptr =
 | 
				
			||||||
 | 
					            unsafe { vb_get_samples(self.sim, ptr::null_mut(), ptr::null_mut(), &mut position) };
 | 
				
			||||||
 | 
					        // SAFETY: position is an offset in a buffer of (f32, f32). so, position * 2 is an offset in a buffer of f32.
 | 
				
			||||||
 | 
					        let read_samples: &mut [f32] =
 | 
				
			||||||
 | 
					            unsafe { slice::from_raw_parts_mut(ptr.cast(), position as usize * 2) };
 | 
				
			||||||
 | 
					        samples.extend_from_slice(read_samples);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        unsafe {
 | 
				
			||||||
 | 
					            vb_set_samples(
 | 
				
			||||||
 | 
					                self.sim,
 | 
				
			||||||
 | 
					                ptr,
 | 
				
			||||||
 | 
					                VBDataType::F32,
 | 
				
			||||||
 | 
					                AUDIO_CAPACITY_SAMPLES as u32,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn set_keys(&mut self, keys: VBKey) {
 | 
				
			||||||
 | 
					        unsafe { vb_set_keys(self.sim, keys.bits()) };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Drop for CoreVB {
 | 
				
			||||||
 | 
					    fn drop(&mut self) {
 | 
				
			||||||
 | 
					        let ptr =
 | 
				
			||||||
 | 
					            unsafe { vb_get_samples(self.sim, ptr::null_mut(), ptr::null_mut(), ptr::null_mut()) };
 | 
				
			||||||
 | 
					        // SAFETY: the audio buffer originally came from a Vec<u32>
 | 
				
			||||||
 | 
					        let floats: Vec<f32> = unsafe {
 | 
				
			||||||
 | 
					            Vec::from_raw_parts(ptr.cast(), AUDIO_CAPACITY_FLOATS, AUDIO_CAPACITY_FLOATS)
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        drop(floats);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // SAFETY: the *mut VB owns its userdata.
 | 
				
			||||||
 | 
					        // There is no way for the userdata to be null or otherwise invalid.
 | 
				
			||||||
 | 
					        let ptr: *mut VBState = unsafe { vb_get_user_data(self.sim).cast() };
 | 
				
			||||||
 | 
					        // SAFETY: we made this pointer ourselves, we can for sure free it
 | 
				
			||||||
 | 
					        unsafe { drop(Box::from_raw(ptr)) };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let len = unsafe { vb_size_of() }.div_ceil(4);
 | 
				
			||||||
 | 
					        // SAFETY: the sim's memory originally came from a Vec<u64>
 | 
				
			||||||
 | 
					        let bytes: Vec<u64> = unsafe { Vec::from_raw_parts(self.sim.cast(), len, len) };
 | 
				
			||||||
 | 
					        drop(bytes);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
		Reference in New Issue