mirror of
https://github.com/lowbit/ps5-doom.git
synced 2026-10-06 06:00:23 +02:00
Normalize line endings to LF
Twenty files had been committed with CRLF endings. .gitattributes now keeps text files LF.
This commit is contained in:
1 parent
61c14ce8a2
commit
9a0c226c42
21 files changed
+3012
-3010
No files matched your search
@@ -0,0 +1,2 @@
|
||||
* text=auto eol=lf
|
||||
*.png binary
|
||||
+140
-140
@@ -1,140 +1,140 @@
|
||||
# DOOM for PS5 (native)
|
||||
|
||||
id Software's original `linuxdoom-1.10` source running as a native PS5 title: its own home-screen
|
||||
icon, launched like a game, drawing through Sony's VideoOut and AGC (GPU) drivers, reading the
|
||||
DualSense through ScePad and playing sound through AudioOut. Title ID `PPSA99666`.
|
||||
|
||||
All PS5, platform, audio and test code here is written for this project. The only external code
|
||||
is id's game source and the build tools listed at the end.
|
||||
|
||||
## Layout
|
||||
|
||||
| Path | What |
|
||||
| --- | --- |
|
||||
| `src/doom/` | id's original game code with the 64-bit and portability fixes listed below |
|
||||
| `src/port/` | Doom's `i_*` layer (main, system, video, sound, network, controller mapping) on top of the platform API |
|
||||
| `src/platform/platform.h` | The platform API: time, log, files, video present, pad, rumble, audio output |
|
||||
| `src/audio/` | Sound engine: SFX mixer, MUS sequencer with DMX-style voice allocation, OPL FM synth |
|
||||
| `src/ps5/` | PS5 backend: startup (`crt0.c`), system, VideoOut, AGC compute presenter, pad, AudioOut |
|
||||
| `src/ps5/present.cl` | GPU kernel: palette lookup, sharp-bilinear scaling to 1080p, tiled scanout writes (gfx1010, wave64) |
|
||||
| `src/host/` | Headless Linux backend for testing on the PC: PNG frames, scripted pad, WAV audio |
|
||||
| `test/` | `render_music` (MUS lump to WAV) and `present_preview` (runs the GPU kernel's math on the CPU) |
|
||||
| `title/` | `param.json` and the icon |
|
||||
| `tools/` | Tool fetch, shareware WAD fetch, kernel embedding, deploy |
|
||||
|
||||
## Build
|
||||
|
||||
Everything builds in the `ps5-doom-build` image (`docker/Dockerfile`: clang/lld/llvm 18, ninja, gdb).
|
||||
|
||||
```bash
|
||||
docker build -t ps5-doom-build docker/
|
||||
MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/src" -w /src ps5-doom-build make -j16 ps5 host
|
||||
```
|
||||
|
||||
`make ps5` fetches the pinned tools and the shareware WAD on first use, then:
|
||||
|
||||
1. compiles the game and the PS5 layer for `x86_64-sie-ps5` against the PS5 payload SDK headers;
|
||||
2. compiles `present.cl` for `gfx1010` (wave64), links it and embeds it with `tools/embed-kernel.py`, which
|
||||
reads the register values from the compiler's kernel descriptor and fails the build if the
|
||||
kernel needs anything the presenter does not set up;
|
||||
3. generates `libSceAgc` / `libSceAgcDriver` import stubs from `src/ps5/stubs/*.txt` (the SDK has none);
|
||||
4. links a PIE, converts it to a PS5 module, signs the FSELF and assembles `dist/PPSA99666/`.
|
||||
|
||||
## Testing on the PC
|
||||
|
||||
`build/host/bin/doom` is the same game on the headless backend. Environment variables:
|
||||
`DOOM_WADDIR`, `DOOM_SAVEDIR`, `DOOM_OUT` (frame output dir), `DOOM_CAPTURE` (frame numbers to
|
||||
save as PNG plus raw indices and palette), `DOOM_FRAMES` (exit after N frames), `DOOM_AUDIO`
|
||||
(absolute WAV path), `DOOM_INPUT` (pad script: `frame:BUTTON+BUTTON+LX=-32000:frames;...`).
|
||||
|
||||
```bash
|
||||
DOOM_OUT=out DOOM_CAPTURE=120,900 build/host/bin/doom -timedemo demo1
|
||||
```
|
||||
|
||||
For memory errors, build with AddressSanitizer:
|
||||
`make host HOST_DIR=build/asan HOST_FLAGS="-O1 -g -fsanitize=address" HOST_CC="clang -fsanitize=address"`.
|
||||
|
||||
## On the console
|
||||
|
||||
`uv run --no-project python tools/deploy.py` uploads `dist/PPSA99666` to `/data/homebrew/PPSA99666`
|
||||
(PS5Upload helper must be running), where ShadowMountPlus registers it. The package bundles Freedoom
|
||||
(`tools/fetch-freedoom.sh`). id's IWADs (`DOOM.WAD`, `DOOM2.WAD`, `PLUTONIA.WAD`, `TNT.WAD`) dropped
|
||||
into `/data/homebrew/PPSA99666/wads/` take priority; order: DOOM II-style id WADs, DOOM-style id
|
||||
WADs, Freedoom 2, Freedoom 1; holding L2 at launch prefers the DOOM-style game. `make release` writes
|
||||
`dist/PPSA99666.zip` and its `.sha256`.
|
||||
|
||||
The app is sandboxed: it reads `/app0` (its folder) and writes `/download0` (config, saves and
|
||||
`doom.log`). Log lines also go to the kernel log (`sceKernelDebugOutText`), readable with klogsrv.
|
||||
Fatal errors show as a system notification.
|
||||
|
||||
Presentation uses the AGC compute path by default. Hold **L1+R1** while the game starts to force the
|
||||
CPU scaler. If a GPU frame does not complete within 500 ms, the game switches to the CPU scaler by
|
||||
itself and logs why.
|
||||
|
||||
## Console testing
|
||||
|
||||
`tools/console_test.py <plan>` runs the deployed title hands-free: it writes the plan as `test.cfg`
|
||||
into the title folder, launches through the `doomlaunch` payload (`tools/launcher`, put into
|
||||
`/data/pldmgr/payloads/doomlaunch/`), pulls captured frames over TCP from the running game (the
|
||||
app listens on 9119; the PC firewall blocks the other direction), de-tiles them to PNG in
|
||||
`build/test/run/`, prints the log from klogsrv (port 3232, captured to `build/test/klog.txt`), then
|
||||
deletes `test.cfg`. Plan lines: `input <steps>`, `capture <frames>`, `frames <N>` (clean exit),
|
||||
`present cpu`. Plans: `test/console-play.cfg` (menus, play, save, load), `console-cpu.cfg`,
|
||||
`console-soak.cfg` (6 minutes).
|
||||
|
||||
## PS5 facts learned on hardware (FW 13.00)
|
||||
|
||||
- `downloadDataSize` 64 is rejected (`0x80a40087`, launch fails); 256 works.
|
||||
- The libc heap of a native app is small: large blocks must come from `mmap` (the zone does).
|
||||
- The sandbox refuses `access`, `chdir`, `opendir`, `dup`, `dup2` (EPERM). `open` works. Saves use
|
||||
explicit `/download0/...` paths; output is captured by pointing `stdout`/`stderr` at a pipe
|
||||
(`fdopen`), and klog wants one line per `sceKernelDebugOutText`.
|
||||
- Scanout must be tiled (`sceVideoOutSetBufferAttribute2` tiling 0; 1 is `0x80290007`), 16 MB per
|
||||
1080p buffer. Pixel layout: 512x128 tiles with the bit interleave in `src/ps5/tiling.h`.
|
||||
- GPU memory: direct memory type 12, protection 0x33, CPU writes flushed with `clflush` before GPU
|
||||
use and invalidated before CPU reads of GPU output (`ps5_cache_flush`).
|
||||
- Compute runs as wave64 whatever the dispatch flag says, and the GPU mishandles the gfx10.3 `null`
|
||||
carry-out operand (writes are dropped). The kernel is built for gfx1010, wave64; `embed-kernel.py`
|
||||
rejects wave32. GPU page faults appear in klog as `GPU Protection fault ... addr(VA)`.
|
||||
- After rest mode ShadowMountPlus can lose its install hook ("TitleDir bridge unavailable");
|
||||
restarting the ShadowMountPlus payload through the Payload Manager fixes it without a reboot.
|
||||
|
||||
## Controls
|
||||
|
||||
| Input | In game | In menus |
|
||||
| --- | --- | --- |
|
||||
| Left stick | Move and strafe (analog) | Navigate |
|
||||
| Right stick | Turn (analog; speed follows the Mouse Sensitivity slider) | |
|
||||
| D-pad | Move and turn (digital) | Navigate (repeats when held) |
|
||||
| R2 | Fire | |
|
||||
| Cross / Square | Use | Select (Cross), answers yes to prompts |
|
||||
| Circle | Strafe modifier | Back, answers no to prompts |
|
||||
| L1 / R1 | Previous / next weapon (zoom on the automap) | |
|
||||
| L2 | Run | |
|
||||
| Triangle / touchpad | Automap (Square toggles follow mode on the map) | |
|
||||
| Options | Menu | Close menu |
|
||||
|
||||
Saving needs no keyboard: an empty slot is pre-filled with the level name, Cross saves.
|
||||
|
||||
## Changes to id's code
|
||||
|
||||
- 64-bit: pointers stored in `int` (config string defaults, save game pointer fields, the
|
||||
`columndirectory` field of the on-disk texture struct), pointer arrays sized `count*4`
|
||||
(`r_data.c`, `p_setup.c`), table alignment through `int` casts, 16-byte zone alignment.
|
||||
- Undefined behaviour and latent bugs: the unterminated `sprnames` list, unsequenced event queue
|
||||
updates, button array resets that cleared 8 bytes of pointers, save games written into the
|
||||
screen buffers (now a dedicated 4 MB buffer).
|
||||
- Portability: no `values.h`/`alloca.h`/sound server; IWAD search through `I_GetWadDirs`; config in
|
||||
the working directory (the port changes into the save directory at start).
|
||||
- Behaviour: 1.9 demos play (the WADs' attract demos); empty save slots get a default name;
|
||||
default SFX channels 8 (the DOS default) instead of 3; Freedoom IWADs recognised; static limits
|
||||
raised (visplanes, drawsegs, sprites, openings, intercepts, plats, ceilings, buttons, scrollers)
|
||||
so limit-removing maps such as Freedoom's run.
|
||||
|
||||
## External tools (pinned)
|
||||
|
||||
- [ps5-native-app-boilerplate](https://github.com/blackbearreloaded/ps5-native-app-boilerplate)
|
||||
`b1315a9`: its `ps5-native-tool` (PIE to PS5 module, FSELF signing), its clean-room `libc.prx`,
|
||||
its intermediate PIE linker script, and the PS5 payload SDK v0.42 it fetches. Built in
|
||||
`.deps/native-app` by `tools/fetch-native-tools.sh`; nothing from it is copied into `src/`.
|
||||
- clang/lld/llvm 18 (x86_64-sie-ps5 and amdgcn targets).
|
||||
# DOOM for PS5 (native)
|
||||
|
||||
id Software's original `linuxdoom-1.10` source running as a native PS5 title: its own home-screen
|
||||
icon, launched like a game, drawing through Sony's VideoOut and AGC (GPU) drivers, reading the
|
||||
DualSense through ScePad and playing sound through AudioOut. Title ID `PPSA99666`.
|
||||
|
||||
All PS5, platform, audio and test code here is written for this project. The only external code
|
||||
is id's game source and the build tools listed at the end.
|
||||
|
||||
## Layout
|
||||
|
||||
| Path | What |
|
||||
| --- | --- |
|
||||
| `src/doom/` | id's original game code with the 64-bit and portability fixes listed below |
|
||||
| `src/port/` | Doom's `i_*` layer (main, system, video, sound, network, controller mapping) on top of the platform API |
|
||||
| `src/platform/platform.h` | The platform API: time, log, files, video present, pad, rumble, audio output |
|
||||
| `src/audio/` | Sound engine: SFX mixer, MUS sequencer with DMX-style voice allocation, OPL FM synth |
|
||||
| `src/ps5/` | PS5 backend: startup (`crt0.c`), system, VideoOut, AGC compute presenter, pad, AudioOut |
|
||||
| `src/ps5/present.cl` | GPU kernel: palette lookup, sharp-bilinear scaling to 1080p, tiled scanout writes (gfx1010, wave64) |
|
||||
| `src/host/` | Headless Linux backend for testing on the PC: PNG frames, scripted pad, WAV audio |
|
||||
| `test/` | `render_music` (MUS lump to WAV) and `present_preview` (runs the GPU kernel's math on the CPU) |
|
||||
| `title/` | `param.json` and the icon |
|
||||
| `tools/` | Tool fetch, shareware WAD fetch, kernel embedding, deploy |
|
||||
|
||||
## Build
|
||||
|
||||
Everything builds in the `ps5-doom-build` image (`docker/Dockerfile`: clang/lld/llvm 18, ninja, gdb).
|
||||
|
||||
```bash
|
||||
docker build -t ps5-doom-build docker/
|
||||
MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/src" -w /src ps5-doom-build make -j16 ps5 host
|
||||
```
|
||||
|
||||
`make ps5` fetches the pinned tools and the shareware WAD on first use, then:
|
||||
|
||||
1. compiles the game and the PS5 layer for `x86_64-sie-ps5` against the PS5 payload SDK headers;
|
||||
2. compiles `present.cl` for `gfx1010` (wave64), links it and embeds it with `tools/embed-kernel.py`, which
|
||||
reads the register values from the compiler's kernel descriptor and fails the build if the
|
||||
kernel needs anything the presenter does not set up;
|
||||
3. generates `libSceAgc` / `libSceAgcDriver` import stubs from `src/ps5/stubs/*.txt` (the SDK has none);
|
||||
4. links a PIE, converts it to a PS5 module, signs the FSELF and assembles `dist/PPSA99666/`.
|
||||
|
||||
## Testing on the PC
|
||||
|
||||
`build/host/bin/doom` is the same game on the headless backend. Environment variables:
|
||||
`DOOM_WADDIR`, `DOOM_SAVEDIR`, `DOOM_OUT` (frame output dir), `DOOM_CAPTURE` (frame numbers to
|
||||
save as PNG plus raw indices and palette), `DOOM_FRAMES` (exit after N frames), `DOOM_AUDIO`
|
||||
(absolute WAV path), `DOOM_INPUT` (pad script: `frame:BUTTON+BUTTON+LX=-32000:frames;...`).
|
||||
|
||||
```bash
|
||||
DOOM_OUT=out DOOM_CAPTURE=120,900 build/host/bin/doom -timedemo demo1
|
||||
```
|
||||
|
||||
For memory errors, build with AddressSanitizer:
|
||||
`make host HOST_DIR=build/asan HOST_FLAGS="-O1 -g -fsanitize=address" HOST_CC="clang -fsanitize=address"`.
|
||||
|
||||
## On the console
|
||||
|
||||
`uv run --no-project python tools/deploy.py` uploads `dist/PPSA99666` to `/data/homebrew/PPSA99666`
|
||||
(PS5Upload helper must be running), where ShadowMountPlus registers it. The package bundles Freedoom
|
||||
(`tools/fetch-freedoom.sh`). id's IWADs (`DOOM.WAD`, `DOOM2.WAD`, `PLUTONIA.WAD`, `TNT.WAD`) dropped
|
||||
into `/data/homebrew/PPSA99666/wads/` take priority; order: DOOM II-style id WADs, DOOM-style id
|
||||
WADs, Freedoom 2, Freedoom 1; holding L2 at launch prefers the DOOM-style game. `make release` writes
|
||||
`dist/PPSA99666.zip` and its `.sha256`.
|
||||
|
||||
The app is sandboxed: it reads `/app0` (its folder) and writes `/download0` (config, saves and
|
||||
`doom.log`). Log lines also go to the kernel log (`sceKernelDebugOutText`), readable with klogsrv.
|
||||
Fatal errors show as a system notification.
|
||||
|
||||
Presentation uses the AGC compute path by default. Hold **L1+R1** while the game starts to force the
|
||||
CPU scaler. If a GPU frame does not complete within 500 ms, the game switches to the CPU scaler by
|
||||
itself and logs why.
|
||||
|
||||
## Console testing
|
||||
|
||||
`tools/console_test.py <plan>` runs the deployed title hands-free: it writes the plan as `test.cfg`
|
||||
into the title folder, launches through the `doomlaunch` payload (`tools/launcher`, put into
|
||||
`/data/pldmgr/payloads/doomlaunch/`), pulls captured frames over TCP from the running game (the
|
||||
app listens on 9119; the PC firewall blocks the other direction), de-tiles them to PNG in
|
||||
`build/test/run/`, prints the log from klogsrv (port 3232, captured to `build/test/klog.txt`), then
|
||||
deletes `test.cfg`. Plan lines: `input <steps>`, `capture <frames>`, `frames <N>` (clean exit),
|
||||
`present cpu`. Plans: `test/console-play.cfg` (menus, play, save, load), `console-cpu.cfg`,
|
||||
`console-soak.cfg` (6 minutes).
|
||||
|
||||
## PS5 facts learned on hardware (FW 13.00)
|
||||
|
||||
- `downloadDataSize` 64 is rejected (`0x80a40087`, launch fails); 256 works.
|
||||
- The libc heap of a native app is small: large blocks must come from `mmap` (the zone does).
|
||||
- The sandbox refuses `access`, `chdir`, `opendir`, `dup`, `dup2` (EPERM). `open` works. Saves use
|
||||
explicit `/download0/...` paths; output is captured by pointing `stdout`/`stderr` at a pipe
|
||||
(`fdopen`), and klog wants one line per `sceKernelDebugOutText`.
|
||||
- Scanout must be tiled (`sceVideoOutSetBufferAttribute2` tiling 0; 1 is `0x80290007`), 16 MB per
|
||||
1080p buffer. Pixel layout: 512x128 tiles with the bit interleave in `src/ps5/tiling.h`.
|
||||
- GPU memory: direct memory type 12, protection 0x33, CPU writes flushed with `clflush` before GPU
|
||||
use and invalidated before CPU reads of GPU output (`ps5_cache_flush`).
|
||||
- Compute runs as wave64 whatever the dispatch flag says, and the GPU mishandles the gfx10.3 `null`
|
||||
carry-out operand (writes are dropped). The kernel is built for gfx1010, wave64; `embed-kernel.py`
|
||||
rejects wave32. GPU page faults appear in klog as `GPU Protection fault ... addr(VA)`.
|
||||
- After rest mode ShadowMountPlus can lose its install hook ("TitleDir bridge unavailable");
|
||||
restarting the ShadowMountPlus payload through the Payload Manager fixes it without a reboot.
|
||||
|
||||
## Controls
|
||||
|
||||
| Input | In game | In menus |
|
||||
| --- | --- | --- |
|
||||
| Left stick | Move and strafe (analog) | Navigate |
|
||||
| Right stick | Turn (analog; speed follows the Mouse Sensitivity slider) | |
|
||||
| D-pad | Move and turn (digital) | Navigate (repeats when held) |
|
||||
| R2 | Fire | |
|
||||
| Cross / Square | Use | Select (Cross), answers yes to prompts |
|
||||
| Circle | Strafe modifier | Back, answers no to prompts |
|
||||
| L1 / R1 | Previous / next weapon (zoom on the automap) | |
|
||||
| L2 | Run | |
|
||||
| Triangle / touchpad | Automap (Square toggles follow mode on the map) | |
|
||||
| Options | Menu | Close menu |
|
||||
|
||||
Saving needs no keyboard: an empty slot is pre-filled with the level name, Cross saves.
|
||||
|
||||
## Changes to id's code
|
||||
|
||||
- 64-bit: pointers stored in `int` (config string defaults, save game pointer fields, the
|
||||
`columndirectory` field of the on-disk texture struct), pointer arrays sized `count*4`
|
||||
(`r_data.c`, `p_setup.c`), table alignment through `int` casts, 16-byte zone alignment.
|
||||
- Undefined behaviour and latent bugs: the unterminated `sprnames` list, unsequenced event queue
|
||||
updates, button array resets that cleared 8 bytes of pointers, save games written into the
|
||||
screen buffers (now a dedicated 4 MB buffer).
|
||||
- Portability: no `values.h`/`alloca.h`/sound server; IWAD search through `I_GetWadDirs`; config in
|
||||
the working directory (the port changes into the save directory at start).
|
||||
- Behaviour: 1.9 demos play (the WADs' attract demos); empty save slots get a default name;
|
||||
default SFX channels 8 (the DOS default) instead of 3; Freedoom IWADs recognised; static limits
|
||||
raised (visplanes, drawsegs, sprites, openings, intercepts, plats, ceilings, buttons, scrollers)
|
||||
so limit-removing maps such as Freedoom's run.
|
||||
|
||||
## External tools (pinned)
|
||||
|
||||
- [ps5-native-app-boilerplate](https://github.com/blackbearreloaded/ps5-native-app-boilerplate)
|
||||
`b1315a9`: its `ps5-native-tool` (PIE to PS5 module, FSELF signing), its clean-room `libc.prx`,
|
||||
its intermediate PIE linker script, and the PS5 payload SDK v0.42 it fetches. Built in
|
||||
`.deps/native-app` by `tools/fetch-native-tools.sh`; nothing from it is copied into `src/`.
|
||||
- clang/lld/llvm 18 (x86_64-sie-ps5 and amdgcn targets).
|
||||
@@ -1,116 +1,116 @@
|
||||
DOOM_SRC := $(wildcard src/doom/*.c)
|
||||
PORT_SRC := $(wildcard src/port/*.c) $(wildcard src/audio/*.c) $(wildcard src/platform/*.c)
|
||||
HOST_SRC := $(wildcard src/host/*.c)
|
||||
PS5_SRC := $(wildcard src/ps5/*.c)
|
||||
|
||||
INCLUDES := -Isrc/doom -Isrc/port -Isrc/platform -Isrc/audio
|
||||
DOOM_WARNINGS := -Wall -Wno-unused-const-variable -Wno-unused-but-set-variable \
|
||||
-Wno-unused-variable -Wno-logical-not-parentheses -Wno-missing-braces
|
||||
OWN_WARNINGS := -Wall -Wextra -Wno-unused-parameter
|
||||
|
||||
HOST_CC := clang
|
||||
HOST_FLAGS := -O2 -g -fno-strict-aliasing -MMD -MP
|
||||
|
||||
HOST_DIR := build/host
|
||||
HOST_OBJ := $(patsubst src/%.c,$(HOST_DIR)/%.o,$(DOOM_SRC) $(PORT_SRC) $(HOST_SRC))
|
||||
|
||||
TITLE := PPSA99666
|
||||
NATIVE := .deps/native-app
|
||||
SDK := $(NATIVE)/.deps/native/ps5-payload-sdk
|
||||
NATIVE_TOOL := $(NATIVE)/build/host/ps5-native-tool
|
||||
RUNTIME := $(NATIVE)/runtime/libc.prx
|
||||
|
||||
PS5_DIR := build/ps5
|
||||
PS5_GEN := $(PS5_DIR)/gen
|
||||
PS5_STUBS := $(PS5_DIR)/stubs
|
||||
PS5_OBJ := $(patsubst src/%.c,$(PS5_DIR)/%.o,$(DOOM_SRC) $(PORT_SRC) $(PS5_SRC))
|
||||
AGC_STUBS := $(patsubst src/ps5/stubs/%.txt,$(PS5_STUBS)/%.so,$(wildcard src/ps5/stubs/*.txt))
|
||||
PS5_CC := clang -target x86_64-sie-ps5 -fvisibility-nodllstorageclass=default \
|
||||
-isysroot $(SDK) -isystem $(SDK)/target/include \
|
||||
-fno-stack-protector -fno-plt -femulated-tls -ffunction-sections -fdata-sections
|
||||
PS5_FLAGS := -O2 -fno-strict-aliasing -MMD -MP
|
||||
APP := dist/$(TITLE)
|
||||
|
||||
.PHONY: host ps5 release tools clean
|
||||
|
||||
host: $(HOST_DIR)/bin/doom
|
||||
|
||||
$(HOST_DIR)/bin/doom: $(HOST_OBJ)
|
||||
@mkdir -p $(dir $@)
|
||||
$(HOST_CC) -o $@ $^ -lm -lpthread
|
||||
|
||||
$(HOST_DIR)/doom/%.o: src/doom/%.c
|
||||
@mkdir -p $(dir $@)
|
||||
$(HOST_CC) -std=gnu99 $(HOST_FLAGS) $(DOOM_WARNINGS) $(INCLUDES) -c $< -o $@
|
||||
|
||||
$(HOST_DIR)/%.o: src/%.c
|
||||
@mkdir -p $(dir $@)
|
||||
$(HOST_CC) -std=gnu11 $(HOST_FLAGS) $(OWN_WARNINGS) $(INCLUDES) -c $< -o $@
|
||||
|
||||
tools: $(NATIVE_TOOL)
|
||||
|
||||
$(NATIVE_TOOL) $(RUNTIME) $(SDK)/target/lib/libkernel.so:
|
||||
bash tools/fetch-native-tools.sh
|
||||
|
||||
wads/DOOM1.WAD:
|
||||
bash tools/fetch-shareware.sh $@
|
||||
|
||||
wads/freedoom2.wad:
|
||||
bash tools/fetch-freedoom.sh wads
|
||||
|
||||
ps5: $(APP)/eboot.bin
|
||||
|
||||
$(PS5_STUBS)/.sdk: $(SDK)/target/lib/libkernel.so
|
||||
@mkdir -p $(PS5_STUBS)
|
||||
cp $(SDK)/target/lib/*.so $(PS5_STUBS)/
|
||||
@touch $@
|
||||
|
||||
$(PS5_STUBS)/%.so: src/ps5/stubs/%.txt $(PS5_STUBS)/.sdk
|
||||
sed 's/.*/void &(void) {}/' $< > $(PS5_STUBS)/$*.c
|
||||
$(PS5_CC) -fPIC -c $(PS5_STUBS)/$*.c -o $(PS5_STUBS)/$*.o
|
||||
$(SDK)/bin/prospero-lld --shared -soname $*.sprx -o $@ $(PS5_STUBS)/$*.o
|
||||
|
||||
$(PS5_GEN)/present_kernel.h: src/ps5/present.cl tools/embed-kernel.py
|
||||
@mkdir -p $(PS5_GEN)
|
||||
clang -target amdgcn-amd-amdhsa -mcpu=gfx1010 -mwavefrontsize64 -x cl -cl-std=CL1.2 -O3 -nogpulib \
|
||||
-c $< -o $(PS5_GEN)/present.o
|
||||
ld.lld -shared $(PS5_GEN)/present.o -o $(PS5_GEN)/present.co
|
||||
python3 tools/embed-kernel.py $(PS5_GEN)/present.co present $@
|
||||
|
||||
$(PS5_DIR)/ps5/gpu.o: $(PS5_GEN)/present_kernel.h
|
||||
|
||||
$(PS5_DIR)/doom/%.o: src/doom/%.c $(SDK)/target/lib/libkernel.so
|
||||
@mkdir -p $(dir $@)
|
||||
$(PS5_CC) -std=gnu99 $(PS5_FLAGS) $(DOOM_WARNINGS) $(INCLUDES) -c $< -o $@
|
||||
|
||||
$(PS5_DIR)/%.o: src/%.c $(SDK)/target/lib/libkernel.so
|
||||
@mkdir -p $(dir $@)
|
||||
$(PS5_CC) -std=gnu11 $(PS5_FLAGS) $(OWN_WARNINGS) $(INCLUDES) -I$(PS5_GEN) -c $< -o $@
|
||||
|
||||
$(PS5_DIR)/llvm-pie.elf: $(PS5_OBJ) $(AGC_STUBS) src/ps5/symbols.map
|
||||
$(SDK)/bin/prospero-lld -T $(NATIVE)/tooling/native/ps5-pie.ld --eh-frame-hdr --gc-sections \
|
||||
--version-script src/ps5/symbols.map -e _start -o $@ $(PS5_OBJ) \
|
||||
--as-needed $(PS5_STUBS)/*.so
|
||||
|
||||
$(PS5_DIR)/eboot.elf: $(PS5_DIR)/llvm-pie.elf $(NATIVE_TOOL)
|
||||
$(NATIVE_TOOL) link --in $< --out $@ --stub-dir $(PS5_STUBS) \
|
||||
--module-sdk 0x02000009 --companion-sdk 0x08050001 --file-name eboot.elf
|
||||
|
||||
$(APP)/eboot.bin: $(PS5_DIR)/eboot.elf $(RUNTIME) title/param.json title/icon0.png wads/freedoom2.wad
|
||||
rm -rf $(APP)
|
||||
mkdir -p $(APP)/sce_sys $(APP)/sce_module $(APP)/wads
|
||||
$(NATIVE_TOOL) self --sign --in $< --out $@ --magic 0x1D3D154F
|
||||
cp $(RUNTIME) $(APP)/sce_module/libc.prx
|
||||
cp title/param.json title/icon0.png $(APP)/sce_sys/
|
||||
cp wads/freedoom1.wad wads/freedoom2.wad wads/FREEDOOM-COPYING.txt wads/FREEDOOM-CREDITS.txt $(APP)/wads/
|
||||
$(NATIVE_TOOL) self --inspect --file $@
|
||||
$(NATIVE_TOOL) self --inspect --file $(APP)/sce_module/libc.prx
|
||||
|
||||
release: $(APP)/eboot.bin
|
||||
rm -f dist/$(TITLE).zip dist/$(TITLE).zip.sha256
|
||||
cd dist && python3 -m zipfile -c $(TITLE).zip $(TITLE) && sha256sum $(TITLE).zip > $(TITLE).zip.sha256
|
||||
|
||||
-include $(HOST_OBJ:.o=.d) $(PS5_OBJ:.o=.d)
|
||||
|
||||
clean:
|
||||
rm -rf build dist
|
||||
DOOM_SRC := $(wildcard src/doom/*.c)
|
||||
PORT_SRC := $(wildcard src/port/*.c) $(wildcard src/audio/*.c) $(wildcard src/platform/*.c)
|
||||
HOST_SRC := $(wildcard src/host/*.c)
|
||||
PS5_SRC := $(wildcard src/ps5/*.c)
|
||||
|
||||
INCLUDES := -Isrc/doom -Isrc/port -Isrc/platform -Isrc/audio
|
||||
DOOM_WARNINGS := -Wall -Wno-unused-const-variable -Wno-unused-but-set-variable \
|
||||
-Wno-unused-variable -Wno-logical-not-parentheses -Wno-missing-braces
|
||||
OWN_WARNINGS := -Wall -Wextra -Wno-unused-parameter
|
||||
|
||||
HOST_CC := clang
|
||||
HOST_FLAGS := -O2 -g -fno-strict-aliasing -MMD -MP
|
||||
|
||||
HOST_DIR := build/host
|
||||
HOST_OBJ := $(patsubst src/%.c,$(HOST_DIR)/%.o,$(DOOM_SRC) $(PORT_SRC) $(HOST_SRC))
|
||||
|
||||
TITLE := PPSA99666
|
||||
NATIVE := .deps/native-app
|
||||
SDK := $(NATIVE)/.deps/native/ps5-payload-sdk
|
||||
NATIVE_TOOL := $(NATIVE)/build/host/ps5-native-tool
|
||||
RUNTIME := $(NATIVE)/runtime/libc.prx
|
||||
|
||||
PS5_DIR := build/ps5
|
||||
PS5_GEN := $(PS5_DIR)/gen
|
||||
PS5_STUBS := $(PS5_DIR)/stubs
|
||||
PS5_OBJ := $(patsubst src/%.c,$(PS5_DIR)/%.o,$(DOOM_SRC) $(PORT_SRC) $(PS5_SRC))
|
||||
AGC_STUBS := $(patsubst src/ps5/stubs/%.txt,$(PS5_STUBS)/%.so,$(wildcard src/ps5/stubs/*.txt))
|
||||
PS5_CC := clang -target x86_64-sie-ps5 -fvisibility-nodllstorageclass=default \
|
||||
-isysroot $(SDK) -isystem $(SDK)/target/include \
|
||||
-fno-stack-protector -fno-plt -femulated-tls -ffunction-sections -fdata-sections
|
||||
PS5_FLAGS := -O2 -fno-strict-aliasing -MMD -MP
|
||||
APP := dist/$(TITLE)
|
||||
|
||||
.PHONY: host ps5 release tools clean
|
||||
|
||||
host: $(HOST_DIR)/bin/doom
|
||||
|
||||
$(HOST_DIR)/bin/doom: $(HOST_OBJ)
|
||||
@mkdir -p $(dir $@)
|
||||
$(HOST_CC) -o $@ $^ -lm -lpthread
|
||||
|
||||
$(HOST_DIR)/doom/%.o: src/doom/%.c
|
||||
@mkdir -p $(dir $@)
|
||||
$(HOST_CC) -std=gnu99 $(HOST_FLAGS) $(DOOM_WARNINGS) $(INCLUDES) -c $< -o $@
|
||||
|
||||
$(HOST_DIR)/%.o: src/%.c
|
||||
@mkdir -p $(dir $@)
|
||||
$(HOST_CC) -std=gnu11 $(HOST_FLAGS) $(OWN_WARNINGS) $(INCLUDES) -c $< -o $@
|
||||
|
||||
tools: $(NATIVE_TOOL)
|
||||
|
||||
$(NATIVE_TOOL) $(RUNTIME) $(SDK)/target/lib/libkernel.so:
|
||||
bash tools/fetch-native-tools.sh
|
||||
|
||||
wads/DOOM1.WAD:
|
||||
bash tools/fetch-shareware.sh $@
|
||||
|
||||
wads/freedoom2.wad:
|
||||
bash tools/fetch-freedoom.sh wads
|
||||
|
||||
ps5: $(APP)/eboot.bin
|
||||
|
||||
$(PS5_STUBS)/.sdk: $(SDK)/target/lib/libkernel.so
|
||||
@mkdir -p $(PS5_STUBS)
|
||||
cp $(SDK)/target/lib/*.so $(PS5_STUBS)/
|
||||
@touch $@
|
||||
|
||||
$(PS5_STUBS)/%.so: src/ps5/stubs/%.txt $(PS5_STUBS)/.sdk
|
||||
sed 's/.*/void &(void) {}/' $< > $(PS5_STUBS)/$*.c
|
||||
$(PS5_CC) -fPIC -c $(PS5_STUBS)/$*.c -o $(PS5_STUBS)/$*.o
|
||||
$(SDK)/bin/prospero-lld --shared -soname $*.sprx -o $@ $(PS5_STUBS)/$*.o
|
||||
|
||||
$(PS5_GEN)/present_kernel.h: src/ps5/present.cl tools/embed-kernel.py
|
||||
@mkdir -p $(PS5_GEN)
|
||||
clang -target amdgcn-amd-amdhsa -mcpu=gfx1010 -mwavefrontsize64 -x cl -cl-std=CL1.2 -O3 -nogpulib \
|
||||
-c $< -o $(PS5_GEN)/present.o
|
||||
ld.lld -shared $(PS5_GEN)/present.o -o $(PS5_GEN)/present.co
|
||||
python3 tools/embed-kernel.py $(PS5_GEN)/present.co present $@
|
||||
|
||||
$(PS5_DIR)/ps5/gpu.o: $(PS5_GEN)/present_kernel.h
|
||||
|
||||
$(PS5_DIR)/doom/%.o: src/doom/%.c $(SDK)/target/lib/libkernel.so
|
||||
@mkdir -p $(dir $@)
|
||||
$(PS5_CC) -std=gnu99 $(PS5_FLAGS) $(DOOM_WARNINGS) $(INCLUDES) -c $< -o $@
|
||||
|
||||
$(PS5_DIR)/%.o: src/%.c $(SDK)/target/lib/libkernel.so
|
||||
@mkdir -p $(dir $@)
|
||||
$(PS5_CC) -std=gnu11 $(PS5_FLAGS) $(OWN_WARNINGS) $(INCLUDES) -I$(PS5_GEN) -c $< -o $@
|
||||
|
||||
$(PS5_DIR)/llvm-pie.elf: $(PS5_OBJ) $(AGC_STUBS) src/ps5/symbols.map
|
||||
$(SDK)/bin/prospero-lld -T $(NATIVE)/tooling/native/ps5-pie.ld --eh-frame-hdr --gc-sections \
|
||||
--version-script src/ps5/symbols.map -e _start -o $@ $(PS5_OBJ) \
|
||||
--as-needed $(PS5_STUBS)/*.so
|
||||
|
||||
$(PS5_DIR)/eboot.elf: $(PS5_DIR)/llvm-pie.elf $(NATIVE_TOOL)
|
||||
$(NATIVE_TOOL) link --in $< --out $@ --stub-dir $(PS5_STUBS) \
|
||||
--module-sdk 0x02000009 --companion-sdk 0x08050001 --file-name eboot.elf
|
||||
|
||||
$(APP)/eboot.bin: $(PS5_DIR)/eboot.elf $(RUNTIME) title/param.json title/icon0.png wads/freedoom2.wad
|
||||
rm -rf $(APP)
|
||||
mkdir -p $(APP)/sce_sys $(APP)/sce_module $(APP)/wads
|
||||
$(NATIVE_TOOL) self --sign --in $< --out $@ --magic 0x1D3D154F
|
||||
cp $(RUNTIME) $(APP)/sce_module/libc.prx
|
||||
cp title/param.json title/icon0.png $(APP)/sce_sys/
|
||||
cp wads/freedoom1.wad wads/freedoom2.wad wads/FREEDOOM-COPYING.txt wads/FREEDOOM-CREDITS.txt $(APP)/wads/
|
||||
$(NATIVE_TOOL) self --inspect --file $@
|
||||
$(NATIVE_TOOL) self --inspect --file $(APP)/sce_module/libc.prx
|
||||
|
||||
release: $(APP)/eboot.bin
|
||||
rm -f dist/$(TITLE).zip dist/$(TITLE).zip.sha256
|
||||
cd dist && python3 -m zipfile -c $(TITLE).zip $(TITLE) && sha256sum $(TITLE).zip > $(TITLE).zip.sha256
|
||||
|
||||
-include $(HOST_OBJ:.o=.d) $(PS5_OBJ:.o=.d)
|
||||
|
||||
clean:
|
||||
rm -rf build dist
|
||||
+235
-235
@@ -1,235 +1,235 @@
|
||||
#include <math.h>
|
||||
#include <pthread.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "audio.h"
|
||||
#include "music.h"
|
||||
#include "platform.h"
|
||||
|
||||
#define SFX_CHANNELS 16
|
||||
#define BLOCK_FRAMES 512
|
||||
#define MUSIC_GAIN 0.7f
|
||||
|
||||
typedef struct
|
||||
{
|
||||
const uint8_t *samples;
|
||||
uint32_t length;
|
||||
uint64_t position, step;
|
||||
int rate, left, right;
|
||||
int handle;
|
||||
} sfx_t;
|
||||
|
||||
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static sfx_t sfx[SFX_CHANNELS];
|
||||
static int serial;
|
||||
static int music_ready;
|
||||
static float music_block[BLOCK_FRAMES];
|
||||
|
||||
static void set_params(sfx_t *s, int volume, int sep, int pitch)
|
||||
{
|
||||
int left, right, swing;
|
||||
|
||||
s->step = (uint64_t)(s->rate * pow(2.0, (pitch - 128) / 64.0) / AUDIO_RATE * 4294967296.0);
|
||||
|
||||
swing = sep + 1;
|
||||
left = volume - ((volume * swing * swing) >> 16);
|
||||
swing -= 257;
|
||||
right = volume - ((volume * swing * swing) >> 16);
|
||||
s->left = left < 0 ? 0 : left;
|
||||
s->right = right < 0 ? 0 : right;
|
||||
}
|
||||
|
||||
static sfx_t *find(int handle)
|
||||
{
|
||||
sfx_t *s;
|
||||
|
||||
if (handle < 0)
|
||||
return NULL;
|
||||
s = &sfx[handle % SFX_CHANNELS];
|
||||
return s->samples && s->handle == handle ? s : NULL;
|
||||
}
|
||||
|
||||
static int clip(int32_t value)
|
||||
{
|
||||
return value > 32767 ? 32767 : value < -32768 ? -32768 : value;
|
||||
}
|
||||
|
||||
static void mix_block(int16_t *out, int frames)
|
||||
{
|
||||
int i, c;
|
||||
|
||||
if (music_ready)
|
||||
music_render(music_block, frames);
|
||||
else
|
||||
memset(music_block, 0, frames * sizeof(*music_block));
|
||||
|
||||
for (i = 0; i < frames; i++)
|
||||
{
|
||||
int32_t music = (int32_t)(music_block[i] * MUSIC_GAIN * 32767.0f);
|
||||
int32_t left = music, right = music;
|
||||
|
||||
for (c = 0; c < SFX_CHANNELS; c++)
|
||||
{
|
||||
sfx_t *s = &sfx[c];
|
||||
uint32_t index;
|
||||
int32_t a, b, sample;
|
||||
|
||||
if (!s->samples)
|
||||
continue;
|
||||
index = (uint32_t)(s->position >> 32);
|
||||
if (index >= s->length)
|
||||
{
|
||||
s->samples = NULL;
|
||||
continue;
|
||||
}
|
||||
a = s->samples[index] - 128;
|
||||
b = index + 1 < s->length ? s->samples[index + 1] - 128 : a;
|
||||
sample = (a << 8) + (int32_t)(((int64_t)(b - a) * (uint32_t)s->position) >> 24);
|
||||
left += sample * s->left / 127;
|
||||
right += sample * s->right / 127;
|
||||
s->position += s->step;
|
||||
}
|
||||
out[i * 2] = (int16_t)clip(left);
|
||||
out[i * 2 + 1] = (int16_t)clip(right);
|
||||
}
|
||||
}
|
||||
|
||||
static void render(int16_t *stereo, int frames, void *user)
|
||||
{
|
||||
(void)user;
|
||||
while (frames > 0)
|
||||
{
|
||||
int count = frames > BLOCK_FRAMES ? BLOCK_FRAMES : frames;
|
||||
|
||||
pthread_mutex_lock(&lock);
|
||||
mix_block(stereo, count);
|
||||
pthread_mutex_unlock(&lock);
|
||||
stereo += count * 2;
|
||||
frames -= count;
|
||||
}
|
||||
}
|
||||
|
||||
int audio_init(const uint8_t *genmidi, int genmidi_length)
|
||||
{
|
||||
music_ready = music_init(AUDIO_RATE, genmidi, genmidi_length) == 0;
|
||||
if (!music_ready)
|
||||
plat_log("audio: no usable GENMIDI, music disabled\n");
|
||||
if (plat_audio_init(AUDIO_RATE, render, NULL))
|
||||
{
|
||||
plat_log("audio: no output device, sound disabled\n");
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void audio_shutdown(void)
|
||||
{
|
||||
plat_audio_shutdown();
|
||||
if (music_ready)
|
||||
music_shutdown();
|
||||
music_ready = 0;
|
||||
}
|
||||
|
||||
int audio_sfx_start(const uint8_t *samples, int length, int rate, int volume, int sep, int pitch)
|
||||
{
|
||||
sfx_t *s = NULL;
|
||||
int c, handle;
|
||||
|
||||
pthread_mutex_lock(&lock);
|
||||
for (c = 0; c < SFX_CHANNELS; c++)
|
||||
if (!sfx[c].samples)
|
||||
{
|
||||
s = &sfx[c];
|
||||
break;
|
||||
}
|
||||
if (!s)
|
||||
{
|
||||
s = &sfx[0];
|
||||
for (c = 1; c < SFX_CHANNELS; c++)
|
||||
if (sfx[c].handle < s->handle)
|
||||
s = &sfx[c];
|
||||
}
|
||||
|
||||
serial++;
|
||||
handle = serial * SFX_CHANNELS + (int)(s - sfx);
|
||||
s->samples = samples;
|
||||
s->length = (uint32_t)length;
|
||||
s->position = 0;
|
||||
s->handle = handle;
|
||||
s->rate = rate;
|
||||
set_params(s, volume, sep, pitch);
|
||||
pthread_mutex_unlock(&lock);
|
||||
return handle;
|
||||
}
|
||||
|
||||
void audio_sfx_stop(int handle)
|
||||
{
|
||||
sfx_t *s;
|
||||
|
||||
pthread_mutex_lock(&lock);
|
||||
if ((s = find(handle)))
|
||||
s->samples = NULL;
|
||||
pthread_mutex_unlock(&lock);
|
||||
}
|
||||
|
||||
int audio_sfx_playing(int handle)
|
||||
{
|
||||
int playing;
|
||||
|
||||
pthread_mutex_lock(&lock);
|
||||
playing = find(handle) != NULL;
|
||||
pthread_mutex_unlock(&lock);
|
||||
return playing;
|
||||
}
|
||||
|
||||
void audio_sfx_update(int handle, int volume, int sep, int pitch)
|
||||
{
|
||||
sfx_t *s;
|
||||
|
||||
pthread_mutex_lock(&lock);
|
||||
if ((s = find(handle)))
|
||||
set_params(s, volume, sep, pitch);
|
||||
pthread_mutex_unlock(&lock);
|
||||
}
|
||||
|
||||
int audio_music_load(const uint8_t *mus)
|
||||
{
|
||||
int result;
|
||||
|
||||
pthread_mutex_lock(&lock);
|
||||
result = music_ready ? music_load(mus) : -1;
|
||||
pthread_mutex_unlock(&lock);
|
||||
return result;
|
||||
}
|
||||
|
||||
void audio_music_play(int looping)
|
||||
{
|
||||
pthread_mutex_lock(&lock);
|
||||
if (music_ready)
|
||||
music_play(looping);
|
||||
pthread_mutex_unlock(&lock);
|
||||
}
|
||||
|
||||
void audio_music_stop(void)
|
||||
{
|
||||
pthread_mutex_lock(&lock);
|
||||
if (music_ready)
|
||||
music_stop();
|
||||
pthread_mutex_unlock(&lock);
|
||||
}
|
||||
|
||||
void audio_music_pause(int paused)
|
||||
{
|
||||
pthread_mutex_lock(&lock);
|
||||
if (music_ready)
|
||||
music_pause(paused);
|
||||
pthread_mutex_unlock(&lock);
|
||||
}
|
||||
|
||||
void audio_music_volume(int volume)
|
||||
{
|
||||
pthread_mutex_lock(&lock);
|
||||
if (music_ready)
|
||||
music_volume(volume);
|
||||
pthread_mutex_unlock(&lock);
|
||||
}
|
||||
#include <math.h>
|
||||
#include <pthread.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "audio.h"
|
||||
#include "music.h"
|
||||
#include "platform.h"
|
||||
|
||||
#define SFX_CHANNELS 16
|
||||
#define BLOCK_FRAMES 512
|
||||
#define MUSIC_GAIN 0.7f
|
||||
|
||||
typedef struct
|
||||
{
|
||||
const uint8_t *samples;
|
||||
uint32_t length;
|
||||
uint64_t position, step;
|
||||
int rate, left, right;
|
||||
int handle;
|
||||
} sfx_t;
|
||||
|
||||
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static sfx_t sfx[SFX_CHANNELS];
|
||||
static int serial;
|
||||
static int music_ready;
|
||||
static float music_block[BLOCK_FRAMES];
|
||||
|
||||
static void set_params(sfx_t *s, int volume, int sep, int pitch)
|
||||
{
|
||||
int left, right, swing;
|
||||
|
||||
s->step = (uint64_t)(s->rate * pow(2.0, (pitch - 128) / 64.0) / AUDIO_RATE * 4294967296.0);
|
||||
|
||||
swing = sep + 1;
|
||||
left = volume - ((volume * swing * swing) >> 16);
|
||||
swing -= 257;
|
||||
right = volume - ((volume * swing * swing) >> 16);
|
||||
s->left = left < 0 ? 0 : left;
|
||||
s->right = right < 0 ? 0 : right;
|
||||
}
|
||||
|
||||
static sfx_t *find(int handle)
|
||||
{
|
||||
sfx_t *s;
|
||||
|
||||
if (handle < 0)
|
||||
return NULL;
|
||||
s = &sfx[handle % SFX_CHANNELS];
|
||||
return s->samples && s->handle == handle ? s : NULL;
|
||||
}
|
||||
|
||||
static int clip(int32_t value)
|
||||
{
|
||||
return value > 32767 ? 32767 : value < -32768 ? -32768 : value;
|
||||
}
|
||||
|
||||
static void mix_block(int16_t *out, int frames)
|
||||
{
|
||||
int i, c;
|
||||
|
||||
if (music_ready)
|
||||
music_render(music_block, frames);
|
||||
else
|
||||
memset(music_block, 0, frames * sizeof(*music_block));
|
||||
|
||||
for (i = 0; i < frames; i++)
|
||||
{
|
||||
int32_t music = (int32_t)(music_block[i] * MUSIC_GAIN * 32767.0f);
|
||||
int32_t left = music, right = music;
|
||||
|
||||
for (c = 0; c < SFX_CHANNELS; c++)
|
||||
{
|
||||
sfx_t *s = &sfx[c];
|
||||
uint32_t index;
|
||||
int32_t a, b, sample;
|
||||
|
||||
if (!s->samples)
|
||||
continue;
|
||||
index = (uint32_t)(s->position >> 32);
|
||||
if (index >= s->length)
|
||||
{
|
||||
s->samples = NULL;
|
||||
continue;
|
||||
}
|
||||
a = s->samples[index] - 128;
|
||||
b = index + 1 < s->length ? s->samples[index + 1] - 128 : a;
|
||||
sample = (a << 8) + (int32_t)(((int64_t)(b - a) * (uint32_t)s->position) >> 24);
|
||||
left += sample * s->left / 127;
|
||||
right += sample * s->right / 127;
|
||||
s->position += s->step;
|
||||
}
|
||||
out[i * 2] = (int16_t)clip(left);
|
||||
out[i * 2 + 1] = (int16_t)clip(right);
|
||||
}
|
||||
}
|
||||
|
||||
static void render(int16_t *stereo, int frames, void *user)
|
||||
{
|
||||
(void)user;
|
||||
while (frames > 0)
|
||||
{
|
||||
int count = frames > BLOCK_FRAMES ? BLOCK_FRAMES : frames;
|
||||
|
||||
pthread_mutex_lock(&lock);
|
||||
mix_block(stereo, count);
|
||||
pthread_mutex_unlock(&lock);
|
||||
stereo += count * 2;
|
||||
frames -= count;
|
||||
}
|
||||
}
|
||||
|
||||
int audio_init(const uint8_t *genmidi, int genmidi_length)
|
||||
{
|
||||
music_ready = music_init(AUDIO_RATE, genmidi, genmidi_length) == 0;
|
||||
if (!music_ready)
|
||||
plat_log("audio: no usable GENMIDI, music disabled\n");
|
||||
if (plat_audio_init(AUDIO_RATE, render, NULL))
|
||||
{
|
||||
plat_log("audio: no output device, sound disabled\n");
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void audio_shutdown(void)
|
||||
{
|
||||
plat_audio_shutdown();
|
||||
if (music_ready)
|
||||
music_shutdown();
|
||||
music_ready = 0;
|
||||
}
|
||||
|
||||
int audio_sfx_start(const uint8_t *samples, int length, int rate, int volume, int sep, int pitch)
|
||||
{
|
||||
sfx_t *s = NULL;
|
||||
int c, handle;
|
||||
|
||||
pthread_mutex_lock(&lock);
|
||||
for (c = 0; c < SFX_CHANNELS; c++)
|
||||
if (!sfx[c].samples)
|
||||
{
|
||||
s = &sfx[c];
|
||||
break;
|
||||
}
|
||||
if (!s)
|
||||
{
|
||||
s = &sfx[0];
|
||||
for (c = 1; c < SFX_CHANNELS; c++)
|
||||
if (sfx[c].handle < s->handle)
|
||||
s = &sfx[c];
|
||||
}
|
||||
|
||||
serial++;
|
||||
handle = serial * SFX_CHANNELS + (int)(s - sfx);
|
||||
s->samples = samples;
|
||||
s->length = (uint32_t)length;
|
||||
s->position = 0;
|
||||
s->handle = handle;
|
||||
s->rate = rate;
|
||||
set_params(s, volume, sep, pitch);
|
||||
pthread_mutex_unlock(&lock);
|
||||
return handle;
|
||||
}
|
||||
|
||||
void audio_sfx_stop(int handle)
|
||||
{
|
||||
sfx_t *s;
|
||||
|
||||
pthread_mutex_lock(&lock);
|
||||
if ((s = find(handle)))
|
||||
s->samples = NULL;
|
||||
pthread_mutex_unlock(&lock);
|
||||
}
|
||||
|
||||
int audio_sfx_playing(int handle)
|
||||
{
|
||||
int playing;
|
||||
|
||||
pthread_mutex_lock(&lock);
|
||||
playing = find(handle) != NULL;
|
||||
pthread_mutex_unlock(&lock);
|
||||
return playing;
|
||||
}
|
||||
|
||||
void audio_sfx_update(int handle, int volume, int sep, int pitch)
|
||||
{
|
||||
sfx_t *s;
|
||||
|
||||
pthread_mutex_lock(&lock);
|
||||
if ((s = find(handle)))
|
||||
set_params(s, volume, sep, pitch);
|
||||
pthread_mutex_unlock(&lock);
|
||||
}
|
||||
|
||||
int audio_music_load(const uint8_t *mus)
|
||||
{
|
||||
int result;
|
||||
|
||||
pthread_mutex_lock(&lock);
|
||||
result = music_ready ? music_load(mus) : -1;
|
||||
pthread_mutex_unlock(&lock);
|
||||
return result;
|
||||
}
|
||||
|
||||
void audio_music_play(int looping)
|
||||
{
|
||||
pthread_mutex_lock(&lock);
|
||||
if (music_ready)
|
||||
music_play(looping);
|
||||
pthread_mutex_unlock(&lock);
|
||||
}
|
||||
|
||||
void audio_music_stop(void)
|
||||
{
|
||||
pthread_mutex_lock(&lock);
|
||||
if (music_ready)
|
||||
music_stop();
|
||||
pthread_mutex_unlock(&lock);
|
||||
}
|
||||
|
||||
void audio_music_pause(int paused)
|
||||
{
|
||||
pthread_mutex_lock(&lock);
|
||||
if (music_ready)
|
||||
music_pause(paused);
|
||||
pthread_mutex_unlock(&lock);
|
||||
}
|
||||
|
||||
void audio_music_volume(int volume)
|
||||
{
|
||||
pthread_mutex_lock(&lock);
|
||||
if (music_ready)
|
||||
music_volume(volume);
|
||||
pthread_mutex_unlock(&lock);
|
||||
}
|
||||
+511
-511
File diff suppressed because it is too large.
Load diff
+408
-408
@@ -1,408 +1,408 @@
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "opl.h"
|
||||
|
||||
#define CHIP_RATE 49716.0
|
||||
#define WAVE_BITS 10
|
||||
#define WAVE_SIZE (1 << WAVE_BITS)
|
||||
#define GAIN_STEPS 1024
|
||||
#define ENV_SILENT 511.0f
|
||||
#define SLOTS (OPL_CHANNELS * 2)
|
||||
#define CHANNEL_SCALE 0.25f
|
||||
#define TREMOLO_HZ 3.7
|
||||
#define VIBRATO_HZ 6.07
|
||||
|
||||
enum
|
||||
{
|
||||
EG_OFF,
|
||||
EG_ATTACK,
|
||||
EG_DECAY,
|
||||
EG_SUSTAIN,
|
||||
EG_RELEASE
|
||||
};
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint8_t tremolo, vibrato, sustained, ksr, mult;
|
||||
uint8_t ksl, level, attack, decay, sustain, release, wave;
|
||||
|
||||
uint32_t phase, step;
|
||||
int stage;
|
||||
float env, ksl_att, sustain_env;
|
||||
float attack_k, decay_inc, release_inc;
|
||||
float out, prev;
|
||||
} slot_t;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint16_t fnum;
|
||||
uint8_t block, key, feedback, additive;
|
||||
slot_t *mod, *car;
|
||||
} channel_t;
|
||||
|
||||
struct opl
|
||||
{
|
||||
double rate_scale;
|
||||
slot_t slots[SLOTS];
|
||||
channel_t channels[OPL_CHANNELS];
|
||||
uint8_t deep_tremolo, deep_vibrato;
|
||||
double tremolo_phase, tremolo_step;
|
||||
double vibrato_phase, vibrato_step;
|
||||
};
|
||||
|
||||
static float wave_table[8][WAVE_SIZE];
|
||||
static float gain_table[GAIN_STEPS];
|
||||
static int tables_ready;
|
||||
|
||||
static const uint8_t mult_x2[16] = {1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 20, 24, 24, 30, 30};
|
||||
static const uint8_t ksl_rom[16] = {0, 32, 40, 45, 48, 51, 53, 55, 56, 58, 59, 60, 61, 62, 63, 64};
|
||||
static const uint8_t ksl_shift[4] = {0, 1, 2, 0};
|
||||
|
||||
static void build_tables(void)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < WAVE_SIZE; i++)
|
||||
{
|
||||
double angle = 2 * M_PI * i / WAVE_SIZE;
|
||||
float s = (float)sin(angle);
|
||||
float s2 = (float)sin(2 * angle);
|
||||
int first_half = i < WAVE_SIZE / 2;
|
||||
int quarter = (i / (WAVE_SIZE / 4)) & 1;
|
||||
int folded = first_half ? i : WAVE_SIZE - 1 - i;
|
||||
|
||||
wave_table[0][i] = s;
|
||||
wave_table[1][i] = first_half ? s : 0;
|
||||
wave_table[2][i] = fabsf(s);
|
||||
wave_table[3][i] = quarter ? 0 : fabsf(s);
|
||||
wave_table[4][i] = first_half ? s2 : 0;
|
||||
wave_table[5][i] = first_half ? fabsf(s2) : 0;
|
||||
wave_table[6][i] = first_half ? 1.0f : -1.0f;
|
||||
wave_table[7][i] = (first_half ? 1.0f : -1.0f) * exp2f(-folded / 32.0f);
|
||||
}
|
||||
for (i = 0; i < GAIN_STEPS; i++)
|
||||
gain_table[i] = exp2f(-i / 32.0f);
|
||||
tables_ready = 1;
|
||||
}
|
||||
|
||||
static int effective_rate(int reg_rate, const slot_t *slot, const channel_t *channel)
|
||||
{
|
||||
int ksv = (channel->block << 1) | ((channel->fnum >> 9) & 1);
|
||||
int rate;
|
||||
|
||||
if (!reg_rate)
|
||||
return 0;
|
||||
rate = reg_rate * 4 + (slot->ksr ? ksv : ksv >> 2);
|
||||
return rate > 63 ? 63 : rate;
|
||||
}
|
||||
|
||||
static double rate_divisor(int rate)
|
||||
{
|
||||
return (double)(1 << (rate >> 2)) * ((rate & 3) + 4) / 4.0;
|
||||
}
|
||||
|
||||
static float linear_increment(int rate, double rate_scale)
|
||||
{
|
||||
double seconds;
|
||||
|
||||
if (!rate)
|
||||
return 0;
|
||||
seconds = 78.56 / rate_divisor(rate);
|
||||
return (float)(ENV_SILENT / (seconds * CHIP_RATE) * rate_scale);
|
||||
}
|
||||
|
||||
static void update_slot(opl_t *opl, slot_t *slot, const channel_t *channel)
|
||||
{
|
||||
int ksl, attack_rate;
|
||||
uint64_t step;
|
||||
|
||||
step = (uint64_t)((double)((uint32_t)channel->fnum << channel->block) *
|
||||
mult_x2[slot->mult] * 2048.0 * opl->rate_scale);
|
||||
slot->step = (uint32_t)step;
|
||||
|
||||
ksl = (ksl_rom[channel->fnum >> 6] << 2) - ((8 - channel->block) << 5);
|
||||
slot->ksl_att = slot->ksl && ksl > 0 ? (float)(ksl >> ksl_shift[slot->ksl]) : 0;
|
||||
|
||||
attack_rate = effective_rate(slot->attack, slot, channel);
|
||||
if (attack_rate >= 60)
|
||||
slot->attack_k = 1;
|
||||
else if (attack_rate == 0)
|
||||
slot->attack_k = 0;
|
||||
else
|
||||
{
|
||||
double samples = 5.65248 / rate_divisor(attack_rate) * CHIP_RATE / opl->rate_scale;
|
||||
slot->attack_k = (float)(1.0 - exp(log(1.0 / ENV_SILENT) / samples));
|
||||
}
|
||||
slot->decay_inc = linear_increment(effective_rate(slot->decay, slot, channel), opl->rate_scale);
|
||||
slot->release_inc = linear_increment(effective_rate(slot->release, slot, channel), opl->rate_scale);
|
||||
slot->sustain_env = (slot->sustain == 15 ? 31 : slot->sustain) * 16.0f;
|
||||
}
|
||||
|
||||
static void update_channel(opl_t *opl, channel_t *channel)
|
||||
{
|
||||
update_slot(opl, channel->mod, channel);
|
||||
update_slot(opl, channel->car, channel);
|
||||
}
|
||||
|
||||
static void key_on(slot_t *slot)
|
||||
{
|
||||
slot->phase = 0;
|
||||
slot->stage = EG_ATTACK;
|
||||
if (slot->attack_k >= 1)
|
||||
{
|
||||
slot->env = 0;
|
||||
slot->stage = EG_DECAY;
|
||||
}
|
||||
}
|
||||
|
||||
static void key_off(slot_t *slot)
|
||||
{
|
||||
if (slot->stage != EG_OFF)
|
||||
slot->stage = EG_RELEASE;
|
||||
}
|
||||
|
||||
static void advance_envelope(slot_t *slot)
|
||||
{
|
||||
switch (slot->stage)
|
||||
{
|
||||
case EG_ATTACK:
|
||||
slot->env -= slot->env * slot->attack_k;
|
||||
if (slot->env < 0.5f)
|
||||
{
|
||||
slot->env = 0;
|
||||
slot->stage = EG_DECAY;
|
||||
}
|
||||
break;
|
||||
case EG_DECAY:
|
||||
slot->env += slot->decay_inc;
|
||||
if (slot->env >= slot->sustain_env)
|
||||
{
|
||||
slot->env = slot->sustain_env;
|
||||
slot->stage = slot->sustained ? EG_SUSTAIN : EG_RELEASE;
|
||||
}
|
||||
break;
|
||||
case EG_RELEASE:
|
||||
slot->env += slot->release_inc;
|
||||
if (slot->env >= ENV_SILENT)
|
||||
{
|
||||
slot->env = ENV_SILENT;
|
||||
slot->stage = EG_OFF;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static float slot_output(slot_t *slot, int modulation, float tremolo, double vibrato)
|
||||
{
|
||||
float att;
|
||||
int index;
|
||||
|
||||
advance_envelope(slot);
|
||||
slot->phase += slot->vibrato ? (uint32_t)(uint64_t)(slot->step * vibrato) : slot->step;
|
||||
|
||||
att = slot->env + slot->level * 4 + slot->ksl_att + (slot->tremolo ? tremolo : 0);
|
||||
if (slot->stage == EG_OFF || att >= GAIN_STEPS)
|
||||
return 0;
|
||||
index = ((int)(slot->phase >> (32 - WAVE_BITS)) + modulation) & (WAVE_SIZE - 1);
|
||||
return wave_table[slot->wave][index] * gain_table[(int)att];
|
||||
}
|
||||
|
||||
static float channel_output(channel_t *channel, float tremolo, double vibrato)
|
||||
{
|
||||
slot_t *mod = channel->mod, *car = channel->car;
|
||||
int feedback = 0;
|
||||
|
||||
if (mod->stage == EG_OFF && car->stage == EG_OFF)
|
||||
{
|
||||
mod->out = mod->prev = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (channel->feedback)
|
||||
feedback = (int)((mod->prev + mod->out) * 4095.0f) >> (9 - channel->feedback);
|
||||
mod->prev = mod->out;
|
||||
mod->out = slot_output(mod, feedback, tremolo, vibrato);
|
||||
|
||||
if (channel->additive)
|
||||
return mod->out + slot_output(car, 0, tremolo, vibrato);
|
||||
return slot_output(car, (int)(mod->out * 4095.0f), tremolo, vibrato);
|
||||
}
|
||||
|
||||
opl_t *opl_create(int sample_rate)
|
||||
{
|
||||
opl_t *opl;
|
||||
|
||||
if (!tables_ready)
|
||||
build_tables();
|
||||
opl = calloc(1, sizeof(*opl));
|
||||
if (!opl)
|
||||
return NULL;
|
||||
opl->rate_scale = CHIP_RATE / sample_rate;
|
||||
opl->tremolo_step = TREMOLO_HZ / sample_rate;
|
||||
opl->vibrato_step = VIBRATO_HZ / sample_rate;
|
||||
opl_reset(opl);
|
||||
return opl;
|
||||
}
|
||||
|
||||
void opl_destroy(opl_t *opl)
|
||||
{
|
||||
free(opl);
|
||||
}
|
||||
|
||||
void opl_reset(opl_t *opl)
|
||||
{
|
||||
int c;
|
||||
|
||||
memset(opl->slots, 0, sizeof(opl->slots));
|
||||
memset(opl->channels, 0, sizeof(opl->channels));
|
||||
for (c = 0; c < OPL_CHANNELS; c++)
|
||||
{
|
||||
int bank = c / 9, local = c % 9;
|
||||
int mod = bank * 18 + (local / 3) * 6 + local % 3;
|
||||
|
||||
opl->channels[c].mod = &opl->slots[mod];
|
||||
opl->channels[c].car = &opl->slots[mod + 3];
|
||||
}
|
||||
for (c = 0; c < SLOTS; c++)
|
||||
{
|
||||
opl->slots[c].env = ENV_SILENT;
|
||||
opl->slots[c].stage = EG_OFF;
|
||||
}
|
||||
for (c = 0; c < OPL_CHANNELS; c++)
|
||||
update_channel(opl, &opl->channels[c]);
|
||||
}
|
||||
|
||||
static channel_t *slot_channel(opl_t *opl, int slot_index)
|
||||
{
|
||||
int bank = slot_index / 18, local = slot_index % 18;
|
||||
int channel = (local / 6) * 3 + (local % 6) % 3;
|
||||
|
||||
return &opl->channels[bank * 9 + channel];
|
||||
}
|
||||
|
||||
static void write_slot(opl_t *opl, int bank, int offset, int group, uint8_t value)
|
||||
{
|
||||
int index;
|
||||
slot_t *slot;
|
||||
|
||||
if ((offset & 7) >= 6 || offset > 0x15)
|
||||
return;
|
||||
index = bank * 18 + (offset >> 3) * 6 + (offset & 7);
|
||||
slot = &opl->slots[index];
|
||||
|
||||
switch (group)
|
||||
{
|
||||
case 0x20:
|
||||
slot->tremolo = (value >> 7) & 1;
|
||||
slot->vibrato = (value >> 6) & 1;
|
||||
slot->sustained = (value >> 5) & 1;
|
||||
slot->ksr = (value >> 4) & 1;
|
||||
slot->mult = value & 15;
|
||||
break;
|
||||
case 0x40:
|
||||
slot->ksl = value >> 6;
|
||||
slot->level = value & 63;
|
||||
break;
|
||||
case 0x60:
|
||||
slot->attack = value >> 4;
|
||||
slot->decay = value & 15;
|
||||
break;
|
||||
case 0x80:
|
||||
slot->sustain = value >> 4;
|
||||
slot->release = value & 15;
|
||||
break;
|
||||
case 0xe0:
|
||||
slot->wave = value & 7;
|
||||
return;
|
||||
}
|
||||
update_slot(opl, slot, slot_channel(opl, index));
|
||||
}
|
||||
|
||||
void opl_write(opl_t *opl, int reg, uint8_t value)
|
||||
{
|
||||
int bank = (reg >> 8) & 1;
|
||||
int r = reg & 0xff;
|
||||
channel_t *channel;
|
||||
|
||||
switch (r & 0xe0)
|
||||
{
|
||||
case 0x20:
|
||||
case 0x40:
|
||||
case 0x60:
|
||||
case 0x80:
|
||||
case 0xe0:
|
||||
write_slot(opl, bank, r & 0x1f, r & 0xe0, value);
|
||||
return;
|
||||
case 0xa0:
|
||||
if (r == 0xbd && !bank)
|
||||
{
|
||||
opl->deep_tremolo = (value >> 7) & 1;
|
||||
opl->deep_vibrato = (value >> 6) & 1;
|
||||
return;
|
||||
}
|
||||
if ((r & 0x0f) > 8)
|
||||
return;
|
||||
channel = &opl->channels[bank * 9 + (r & 0x0f)];
|
||||
if (r < 0xb0)
|
||||
channel->fnum = (channel->fnum & 0x300) | value;
|
||||
else
|
||||
{
|
||||
uint8_t key = (value >> 5) & 1;
|
||||
|
||||
channel->fnum = (channel->fnum & 0xff) | (value & 3) << 8;
|
||||
channel->block = (value >> 2) & 7;
|
||||
update_channel(opl, channel);
|
||||
if (key && !channel->key)
|
||||
{
|
||||
key_on(channel->mod);
|
||||
key_on(channel->car);
|
||||
}
|
||||
else if (!key && channel->key)
|
||||
{
|
||||
key_off(channel->mod);
|
||||
key_off(channel->car);
|
||||
}
|
||||
channel->key = key;
|
||||
return;
|
||||
}
|
||||
update_channel(opl, channel);
|
||||
return;
|
||||
case 0xc0:
|
||||
if ((r & 0x0f) > 8 || r >= 0xd0)
|
||||
return;
|
||||
channel = &opl->channels[bank * 9 + (r & 0x0f)];
|
||||
channel->feedback = (value >> 1) & 7;
|
||||
channel->additive = value & 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void opl_render(opl_t *opl, float *out, int frames)
|
||||
{
|
||||
float tremolo_depth = opl->deep_tremolo ? 25.6f : 5.3f;
|
||||
double vibrato_depth = opl->deep_vibrato ? 0.0081 : 0.0041;
|
||||
int i, c;
|
||||
|
||||
for (i = 0; i < frames; i++)
|
||||
{
|
||||
double tri = opl->tremolo_phase < 0.5 ? opl->tremolo_phase * 2 : 2 - opl->tremolo_phase * 2;
|
||||
float tremolo = (float)(tri * tremolo_depth);
|
||||
double vibrato = 1.0 + vibrato_depth * sin(2 * M_PI * opl->vibrato_phase);
|
||||
float sum = 0;
|
||||
|
||||
for (c = 0; c < OPL_CHANNELS; c++)
|
||||
sum += channel_output(&opl->channels[c], tremolo, vibrato);
|
||||
out[i] = sum * CHANNEL_SCALE;
|
||||
|
||||
opl->tremolo_phase += opl->tremolo_step;
|
||||
if (opl->tremolo_phase >= 1)
|
||||
opl->tremolo_phase -= 1;
|
||||
opl->vibrato_phase += opl->vibrato_step;
|
||||
if (opl->vibrato_phase >= 1)
|
||||
opl->vibrato_phase -= 1;
|
||||
}
|
||||
}
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "opl.h"
|
||||
|
||||
#define CHIP_RATE 49716.0
|
||||
#define WAVE_BITS 10
|
||||
#define WAVE_SIZE (1 << WAVE_BITS)
|
||||
#define GAIN_STEPS 1024
|
||||
#define ENV_SILENT 511.0f
|
||||
#define SLOTS (OPL_CHANNELS * 2)
|
||||
#define CHANNEL_SCALE 0.25f
|
||||
#define TREMOLO_HZ 3.7
|
||||
#define VIBRATO_HZ 6.07
|
||||
|
||||
enum
|
||||
{
|
||||
EG_OFF,
|
||||
EG_ATTACK,
|
||||
EG_DECAY,
|
||||
EG_SUSTAIN,
|
||||
EG_RELEASE
|
||||
};
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint8_t tremolo, vibrato, sustained, ksr, mult;
|
||||
uint8_t ksl, level, attack, decay, sustain, release, wave;
|
||||
|
||||
uint32_t phase, step;
|
||||
int stage;
|
||||
float env, ksl_att, sustain_env;
|
||||
float attack_k, decay_inc, release_inc;
|
||||
float out, prev;
|
||||
} slot_t;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint16_t fnum;
|
||||
uint8_t block, key, feedback, additive;
|
||||
slot_t *mod, *car;
|
||||
} channel_t;
|
||||
|
||||
struct opl
|
||||
{
|
||||
double rate_scale;
|
||||
slot_t slots[SLOTS];
|
||||
channel_t channels[OPL_CHANNELS];
|
||||
uint8_t deep_tremolo, deep_vibrato;
|
||||
double tremolo_phase, tremolo_step;
|
||||
double vibrato_phase, vibrato_step;
|
||||
};
|
||||
|
||||
static float wave_table[8][WAVE_SIZE];
|
||||
static float gain_table[GAIN_STEPS];
|
||||
static int tables_ready;
|
||||
|
||||
static const uint8_t mult_x2[16] = {1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 20, 24, 24, 30, 30};
|
||||
static const uint8_t ksl_rom[16] = {0, 32, 40, 45, 48, 51, 53, 55, 56, 58, 59, 60, 61, 62, 63, 64};
|
||||
static const uint8_t ksl_shift[4] = {0, 1, 2, 0};
|
||||
|
||||
static void build_tables(void)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < WAVE_SIZE; i++)
|
||||
{
|
||||
double angle = 2 * M_PI * i / WAVE_SIZE;
|
||||
float s = (float)sin(angle);
|
||||
float s2 = (float)sin(2 * angle);
|
||||
int first_half = i < WAVE_SIZE / 2;
|
||||
int quarter = (i / (WAVE_SIZE / 4)) & 1;
|
||||
int folded = first_half ? i : WAVE_SIZE - 1 - i;
|
||||
|
||||
wave_table[0][i] = s;
|
||||
wave_table[1][i] = first_half ? s : 0;
|
||||
wave_table[2][i] = fabsf(s);
|
||||
wave_table[3][i] = quarter ? 0 : fabsf(s);
|
||||
wave_table[4][i] = first_half ? s2 : 0;
|
||||
wave_table[5][i] = first_half ? fabsf(s2) : 0;
|
||||
wave_table[6][i] = first_half ? 1.0f : -1.0f;
|
||||
wave_table[7][i] = (first_half ? 1.0f : -1.0f) * exp2f(-folded / 32.0f);
|
||||
}
|
||||
for (i = 0; i < GAIN_STEPS; i++)
|
||||
gain_table[i] = exp2f(-i / 32.0f);
|
||||
tables_ready = 1;
|
||||
}
|
||||
|
||||
static int effective_rate(int reg_rate, const slot_t *slot, const channel_t *channel)
|
||||
{
|
||||
int ksv = (channel->block << 1) | ((channel->fnum >> 9) & 1);
|
||||
int rate;
|
||||
|
||||
if (!reg_rate)
|
||||
return 0;
|
||||
rate = reg_rate * 4 + (slot->ksr ? ksv : ksv >> 2);
|
||||
return rate > 63 ? 63 : rate;
|
||||
}
|
||||
|
||||
static double rate_divisor(int rate)
|
||||
{
|
||||
return (double)(1 << (rate >> 2)) * ((rate & 3) + 4) / 4.0;
|
||||
}
|
||||
|
||||
static float linear_increment(int rate, double rate_scale)
|
||||
{
|
||||
double seconds;
|
||||
|
||||
if (!rate)
|
||||
return 0;
|
||||
seconds = 78.56 / rate_divisor(rate);
|
||||
return (float)(ENV_SILENT / (seconds * CHIP_RATE) * rate_scale);
|
||||
}
|
||||
|
||||
static void update_slot(opl_t *opl, slot_t *slot, const channel_t *channel)
|
||||
{
|
||||
int ksl, attack_rate;
|
||||
uint64_t step;
|
||||
|
||||
step = (uint64_t)((double)((uint32_t)channel->fnum << channel->block) *
|
||||
mult_x2[slot->mult] * 2048.0 * opl->rate_scale);
|
||||
slot->step = (uint32_t)step;
|
||||
|
||||
ksl = (ksl_rom[channel->fnum >> 6] << 2) - ((8 - channel->block) << 5);
|
||||
slot->ksl_att = slot->ksl && ksl > 0 ? (float)(ksl >> ksl_shift[slot->ksl]) : 0;
|
||||
|
||||
attack_rate = effective_rate(slot->attack, slot, channel);
|
||||
if (attack_rate >= 60)
|
||||
slot->attack_k = 1;
|
||||
else if (attack_rate == 0)
|
||||
slot->attack_k = 0;
|
||||
else
|
||||
{
|
||||
double samples = 5.65248 / rate_divisor(attack_rate) * CHIP_RATE / opl->rate_scale;
|
||||
slot->attack_k = (float)(1.0 - exp(log(1.0 / ENV_SILENT) / samples));
|
||||
}
|
||||
slot->decay_inc = linear_increment(effective_rate(slot->decay, slot, channel), opl->rate_scale);
|
||||
slot->release_inc = linear_increment(effective_rate(slot->release, slot, channel), opl->rate_scale);
|
||||
slot->sustain_env = (slot->sustain == 15 ? 31 : slot->sustain) * 16.0f;
|
||||
}
|
||||
|
||||
static void update_channel(opl_t *opl, channel_t *channel)
|
||||
{
|
||||
update_slot(opl, channel->mod, channel);
|
||||
update_slot(opl, channel->car, channel);
|
||||
}
|
||||
|
||||
static void key_on(slot_t *slot)
|
||||
{
|
||||
slot->phase = 0;
|
||||
slot->stage = EG_ATTACK;
|
||||
if (slot->attack_k >= 1)
|
||||
{
|
||||
slot->env = 0;
|
||||
slot->stage = EG_DECAY;
|
||||
}
|
||||
}
|
||||
|
||||
static void key_off(slot_t *slot)
|
||||
{
|
||||
if (slot->stage != EG_OFF)
|
||||
slot->stage = EG_RELEASE;
|
||||
}
|
||||
|
||||
static void advance_envelope(slot_t *slot)
|
||||
{
|
||||
switch (slot->stage)
|
||||
{
|
||||
case EG_ATTACK:
|
||||
slot->env -= slot->env * slot->attack_k;
|
||||
if (slot->env < 0.5f)
|
||||
{
|
||||
slot->env = 0;
|
||||
slot->stage = EG_DECAY;
|
||||
}
|
||||
break;
|
||||
case EG_DECAY:
|
||||
slot->env += slot->decay_inc;
|
||||
if (slot->env >= slot->sustain_env)
|
||||
{
|
||||
slot->env = slot->sustain_env;
|
||||
slot->stage = slot->sustained ? EG_SUSTAIN : EG_RELEASE;
|
||||
}
|
||||
break;
|
||||
case EG_RELEASE:
|
||||
slot->env += slot->release_inc;
|
||||
if (slot->env >= ENV_SILENT)
|
||||
{
|
||||
slot->env = ENV_SILENT;
|
||||
slot->stage = EG_OFF;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static float slot_output(slot_t *slot, int modulation, float tremolo, double vibrato)
|
||||
{
|
||||
float att;
|
||||
int index;
|
||||
|
||||
advance_envelope(slot);
|
||||
slot->phase += slot->vibrato ? (uint32_t)(uint64_t)(slot->step * vibrato) : slot->step;
|
||||
|
||||
att = slot->env + slot->level * 4 + slot->ksl_att + (slot->tremolo ? tremolo : 0);
|
||||
if (slot->stage == EG_OFF || att >= GAIN_STEPS)
|
||||
return 0;
|
||||
index = ((int)(slot->phase >> (32 - WAVE_BITS)) + modulation) & (WAVE_SIZE - 1);
|
||||
return wave_table[slot->wave][index] * gain_table[(int)att];
|
||||
}
|
||||
|
||||
static float channel_output(channel_t *channel, float tremolo, double vibrato)
|
||||
{
|
||||
slot_t *mod = channel->mod, *car = channel->car;
|
||||
int feedback = 0;
|
||||
|
||||
if (mod->stage == EG_OFF && car->stage == EG_OFF)
|
||||
{
|
||||
mod->out = mod->prev = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (channel->feedback)
|
||||
feedback = (int)((mod->prev + mod->out) * 4095.0f) >> (9 - channel->feedback);
|
||||
mod->prev = mod->out;
|
||||
mod->out = slot_output(mod, feedback, tremolo, vibrato);
|
||||
|
||||
if (channel->additive)
|
||||
return mod->out + slot_output(car, 0, tremolo, vibrato);
|
||||
return slot_output(car, (int)(mod->out * 4095.0f), tremolo, vibrato);
|
||||
}
|
||||
|
||||
opl_t *opl_create(int sample_rate)
|
||||
{
|
||||
opl_t *opl;
|
||||
|
||||
if (!tables_ready)
|
||||
build_tables();
|
||||
opl = calloc(1, sizeof(*opl));
|
||||
if (!opl)
|
||||
return NULL;
|
||||
opl->rate_scale = CHIP_RATE / sample_rate;
|
||||
opl->tremolo_step = TREMOLO_HZ / sample_rate;
|
||||
opl->vibrato_step = VIBRATO_HZ / sample_rate;
|
||||
opl_reset(opl);
|
||||
return opl;
|
||||
}
|
||||
|
||||
void opl_destroy(opl_t *opl)
|
||||
{
|
||||
free(opl);
|
||||
}
|
||||
|
||||
void opl_reset(opl_t *opl)
|
||||
{
|
||||
int c;
|
||||
|
||||
memset(opl->slots, 0, sizeof(opl->slots));
|
||||
memset(opl->channels, 0, sizeof(opl->channels));
|
||||
for (c = 0; c < OPL_CHANNELS; c++)
|
||||
{
|
||||
int bank = c / 9, local = c % 9;
|
||||
int mod = bank * 18 + (local / 3) * 6 + local % 3;
|
||||
|
||||
opl->channels[c].mod = &opl->slots[mod];
|
||||
opl->channels[c].car = &opl->slots[mod + 3];
|
||||
}
|
||||
for (c = 0; c < SLOTS; c++)
|
||||
{
|
||||
opl->slots[c].env = ENV_SILENT;
|
||||
opl->slots[c].stage = EG_OFF;
|
||||
}
|
||||
for (c = 0; c < OPL_CHANNELS; c++)
|
||||
update_channel(opl, &opl->channels[c]);
|
||||
}
|
||||
|
||||
static channel_t *slot_channel(opl_t *opl, int slot_index)
|
||||
{
|
||||
int bank = slot_index / 18, local = slot_index % 18;
|
||||
int channel = (local / 6) * 3 + (local % 6) % 3;
|
||||
|
||||
return &opl->channels[bank * 9 + channel];
|
||||
}
|
||||
|
||||
static void write_slot(opl_t *opl, int bank, int offset, int group, uint8_t value)
|
||||
{
|
||||
int index;
|
||||
slot_t *slot;
|
||||
|
||||
if ((offset & 7) >= 6 || offset > 0x15)
|
||||
return;
|
||||
index = bank * 18 + (offset >> 3) * 6 + (offset & 7);
|
||||
slot = &opl->slots[index];
|
||||
|
||||
switch (group)
|
||||
{
|
||||
case 0x20:
|
||||
slot->tremolo = (value >> 7) & 1;
|
||||
slot->vibrato = (value >> 6) & 1;
|
||||
slot->sustained = (value >> 5) & 1;
|
||||
slot->ksr = (value >> 4) & 1;
|
||||
slot->mult = value & 15;
|
||||
break;
|
||||
case 0x40:
|
||||
slot->ksl = value >> 6;
|
||||
slot->level = value & 63;
|
||||
break;
|
||||
case 0x60:
|
||||
slot->attack = value >> 4;
|
||||
slot->decay = value & 15;
|
||||
break;
|
||||
case 0x80:
|
||||
slot->sustain = value >> 4;
|
||||
slot->release = value & 15;
|
||||
break;
|
||||
case 0xe0:
|
||||
slot->wave = value & 7;
|
||||
return;
|
||||
}
|
||||
update_slot(opl, slot, slot_channel(opl, index));
|
||||
}
|
||||
|
||||
void opl_write(opl_t *opl, int reg, uint8_t value)
|
||||
{
|
||||
int bank = (reg >> 8) & 1;
|
||||
int r = reg & 0xff;
|
||||
channel_t *channel;
|
||||
|
||||
switch (r & 0xe0)
|
||||
{
|
||||
case 0x20:
|
||||
case 0x40:
|
||||
case 0x60:
|
||||
case 0x80:
|
||||
case 0xe0:
|
||||
write_slot(opl, bank, r & 0x1f, r & 0xe0, value);
|
||||
return;
|
||||
case 0xa0:
|
||||
if (r == 0xbd && !bank)
|
||||
{
|
||||
opl->deep_tremolo = (value >> 7) & 1;
|
||||
opl->deep_vibrato = (value >> 6) & 1;
|
||||
return;
|
||||
}
|
||||
if ((r & 0x0f) > 8)
|
||||
return;
|
||||
channel = &opl->channels[bank * 9 + (r & 0x0f)];
|
||||
if (r < 0xb0)
|
||||
channel->fnum = (channel->fnum & 0x300) | value;
|
||||
else
|
||||
{
|
||||
uint8_t key = (value >> 5) & 1;
|
||||
|
||||
channel->fnum = (channel->fnum & 0xff) | (value & 3) << 8;
|
||||
channel->block = (value >> 2) & 7;
|
||||
update_channel(opl, channel);
|
||||
if (key && !channel->key)
|
||||
{
|
||||
key_on(channel->mod);
|
||||
key_on(channel->car);
|
||||
}
|
||||
else if (!key && channel->key)
|
||||
{
|
||||
key_off(channel->mod);
|
||||
key_off(channel->car);
|
||||
}
|
||||
channel->key = key;
|
||||
return;
|
||||
}
|
||||
update_channel(opl, channel);
|
||||
return;
|
||||
case 0xc0:
|
||||
if ((r & 0x0f) > 8 || r >= 0xd0)
|
||||
return;
|
||||
channel = &opl->channels[bank * 9 + (r & 0x0f)];
|
||||
channel->feedback = (value >> 1) & 7;
|
||||
channel->additive = value & 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void opl_render(opl_t *opl, float *out, int frames)
|
||||
{
|
||||
float tremolo_depth = opl->deep_tremolo ? 25.6f : 5.3f;
|
||||
double vibrato_depth = opl->deep_vibrato ? 0.0081 : 0.0041;
|
||||
int i, c;
|
||||
|
||||
for (i = 0; i < frames; i++)
|
||||
{
|
||||
double tri = opl->tremolo_phase < 0.5 ? opl->tremolo_phase * 2 : 2 - opl->tremolo_phase * 2;
|
||||
float tremolo = (float)(tri * tremolo_depth);
|
||||
double vibrato = 1.0 + vibrato_depth * sin(2 * M_PI * opl->vibrato_phase);
|
||||
float sum = 0;
|
||||
|
||||
for (c = 0; c < OPL_CHANNELS; c++)
|
||||
sum += channel_output(&opl->channels[c], tremolo, vibrato);
|
||||
out[i] = sum * CHANNEL_SCALE;
|
||||
|
||||
opl->tremolo_phase += opl->tremolo_step;
|
||||
if (opl->tremolo_phase >= 1)
|
||||
opl->tremolo_phase -= 1;
|
||||
opl->vibrato_phase += opl->vibrato_step;
|
||||
if (opl->vibrato_phase >= 1)
|
||||
opl->vibrato_phase -= 1;
|
||||
}
|
||||
}
|
||||
+56
-56
@@ -1,56 +1,56 @@
|
||||
#ifndef PLATFORM_H
|
||||
#define PLATFORM_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
enum
|
||||
{
|
||||
PAD_UP = 1 << 0,
|
||||
PAD_DOWN = 1 << 1,
|
||||
PAD_LEFT = 1 << 2,
|
||||
PAD_RIGHT = 1 << 3,
|
||||
PAD_CROSS = 1 << 4,
|
||||
PAD_CIRCLE = 1 << 5,
|
||||
PAD_SQUARE = 1 << 6,
|
||||
PAD_TRIANGLE = 1 << 7,
|
||||
PAD_L1 = 1 << 8,
|
||||
PAD_R1 = 1 << 9,
|
||||
PAD_L2 = 1 << 10,
|
||||
PAD_R2 = 1 << 11,
|
||||
PAD_L3 = 1 << 12,
|
||||
PAD_R3 = 1 << 13,
|
||||
PAD_OPTIONS = 1 << 14,
|
||||
PAD_TOUCHPAD = 1 << 15,
|
||||
};
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint32_t buttons;
|
||||
int16_t lx, ly, rx, ry;
|
||||
} pad_state_t;
|
||||
|
||||
typedef void (*audio_render_fn)(int16_t *stereo, int frames, void *user);
|
||||
|
||||
int plat_init(int argc, char **argv);
|
||||
void plat_shutdown(void);
|
||||
_Noreturn void plat_exit(int code);
|
||||
|
||||
void plat_log(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||
void plat_alert(const char *message);
|
||||
uint64_t plat_ticks_us(void);
|
||||
void plat_sleep_us(uint32_t us);
|
||||
|
||||
const char *const *plat_wad_dirs(void);
|
||||
const char *plat_save_dir(void);
|
||||
|
||||
int plat_video_init(int width, int height);
|
||||
void plat_video_present(const uint8_t *pixels, const uint32_t *palette);
|
||||
void plat_video_shutdown(void);
|
||||
|
||||
int plat_pad_read(pad_state_t *state);
|
||||
void plat_pad_rumble(uint8_t strong, uint8_t weak);
|
||||
|
||||
int plat_audio_init(int rate, audio_render_fn render, void *user);
|
||||
void plat_audio_shutdown(void);
|
||||
|
||||
#endif
|
||||
#ifndef PLATFORM_H
|
||||
#define PLATFORM_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
enum
|
||||
{
|
||||
PAD_UP = 1 << 0,
|
||||
PAD_DOWN = 1 << 1,
|
||||
PAD_LEFT = 1 << 2,
|
||||
PAD_RIGHT = 1 << 3,
|
||||
PAD_CROSS = 1 << 4,
|
||||
PAD_CIRCLE = 1 << 5,
|
||||
PAD_SQUARE = 1 << 6,
|
||||
PAD_TRIANGLE = 1 << 7,
|
||||
PAD_L1 = 1 << 8,
|
||||
PAD_R1 = 1 << 9,
|
||||
PAD_L2 = 1 << 10,
|
||||
PAD_R2 = 1 << 11,
|
||||
PAD_L3 = 1 << 12,
|
||||
PAD_R3 = 1 << 13,
|
||||
PAD_OPTIONS = 1 << 14,
|
||||
PAD_TOUCHPAD = 1 << 15,
|
||||
};
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint32_t buttons;
|
||||
int16_t lx, ly, rx, ry;
|
||||
} pad_state_t;
|
||||
|
||||
typedef void (*audio_render_fn)(int16_t *stereo, int frames, void *user);
|
||||
|
||||
int plat_init(int argc, char **argv);
|
||||
void plat_shutdown(void);
|
||||
_Noreturn void plat_exit(int code);
|
||||
|
||||
void plat_log(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||
void plat_alert(const char *message);
|
||||
uint64_t plat_ticks_us(void);
|
||||
void plat_sleep_us(uint32_t us);
|
||||
|
||||
const char *const *plat_wad_dirs(void);
|
||||
const char *plat_save_dir(void);
|
||||
|
||||
int plat_video_init(int width, int height);
|
||||
void plat_video_present(const uint8_t *pixels, const uint32_t *palette);
|
||||
void plat_video_shutdown(void);
|
||||
|
||||
int plat_pad_read(pad_state_t *state);
|
||||
void plat_pad_rumble(uint8_t strong, uint8_t weak);
|
||||
|
||||
int plat_audio_init(int rate, audio_render_fn render, void *user);
|
||||
void plat_audio_shutdown(void);
|
||||
|
||||
#endif
|
||||
+146
-146
@@ -1,146 +1,146 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "test_plan.h"
|
||||
|
||||
#define MAX_STEPS 128
|
||||
#define MAX_CAPTURES 64
|
||||
|
||||
typedef struct
|
||||
{
|
||||
int start, count;
|
||||
uint32_t buttons;
|
||||
int16_t lx, ly, rx, ry;
|
||||
} step_t;
|
||||
|
||||
static const struct
|
||||
{
|
||||
const char *name;
|
||||
uint32_t bit;
|
||||
} names[] = {
|
||||
{"UP", PAD_UP}, {"DOWN", PAD_DOWN}, {"LEFT", PAD_LEFT}, {"RIGHT", PAD_RIGHT},
|
||||
{"CROSS", PAD_CROSS}, {"CIRCLE", PAD_CIRCLE}, {"SQUARE", PAD_SQUARE},
|
||||
{"TRIANGLE", PAD_TRIANGLE}, {"L1", PAD_L1}, {"R1", PAD_R1}, {"L2", PAD_L2},
|
||||
{"R2", PAD_R2}, {"L3", PAD_L3}, {"R3", PAD_R3}, {"OPTIONS", PAD_OPTIONS},
|
||||
{"TOUCHPAD", PAD_TOUCHPAD},
|
||||
};
|
||||
|
||||
static step_t steps[MAX_STEPS];
|
||||
static int step_count;
|
||||
static int captures[MAX_CAPTURES];
|
||||
static int capture_count;
|
||||
static int frame_limit;
|
||||
|
||||
static void parse_token(step_t *step, const char *token)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < sizeof(names) / sizeof(names[0]); i++)
|
||||
if (!strcmp(token, names[i].name))
|
||||
step->buttons |= names[i].bit;
|
||||
if (!strncmp(token, "LX=", 3))
|
||||
step->lx = (int16_t)atoi(token + 3);
|
||||
if (!strncmp(token, "LY=", 3))
|
||||
step->ly = (int16_t)atoi(token + 3);
|
||||
if (!strncmp(token, "RX=", 3))
|
||||
step->rx = (int16_t)atoi(token + 3);
|
||||
if (!strncmp(token, "RY=", 3))
|
||||
step->ry = (int16_t)atoi(token + 3);
|
||||
}
|
||||
|
||||
void test_plan_input(const char *text)
|
||||
{
|
||||
char *copy, *entry, *save_entry;
|
||||
|
||||
step_count = 0;
|
||||
if (!text || !(copy = strdup(text)))
|
||||
return;
|
||||
for (entry = strtok_r(copy, ";", &save_entry); entry && step_count < MAX_STEPS;
|
||||
entry = strtok_r(NULL, ";", &save_entry))
|
||||
{
|
||||
step_t *step = &steps[step_count];
|
||||
char *field, *save_field;
|
||||
int index = 0;
|
||||
|
||||
memset(step, 0, sizeof(*step));
|
||||
for (field = strtok_r(entry, ":", &save_field); field;
|
||||
field = strtok_r(NULL, ":", &save_field), index++)
|
||||
{
|
||||
if (index == 0)
|
||||
step->start = atoi(field);
|
||||
else if (index == 1)
|
||||
{
|
||||
char *token, *save_token;
|
||||
|
||||
for (token = strtok_r(field, "+", &save_token); token;
|
||||
token = strtok_r(NULL, "+", &save_token))
|
||||
parse_token(step, token);
|
||||
}
|
||||
else
|
||||
step->count = atoi(field);
|
||||
}
|
||||
if (!step->count)
|
||||
step->count = 1;
|
||||
step_count++;
|
||||
}
|
||||
free(copy);
|
||||
}
|
||||
|
||||
int test_plan_has_input(void)
|
||||
{
|
||||
return step_count > 0;
|
||||
}
|
||||
|
||||
void test_plan_apply(int frame, pad_state_t *state)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < step_count; i++)
|
||||
{
|
||||
const step_t *step = &steps[i];
|
||||
|
||||
if (frame < step->start || frame >= step->start + step->count)
|
||||
continue;
|
||||
state->buttons |= step->buttons;
|
||||
if (step->lx)
|
||||
state->lx = step->lx;
|
||||
if (step->ly)
|
||||
state->ly = step->ly;
|
||||
if (step->rx)
|
||||
state->rx = step->rx;
|
||||
if (step->ry)
|
||||
state->ry = step->ry;
|
||||
}
|
||||
}
|
||||
|
||||
void test_plan_captures(const char *frames)
|
||||
{
|
||||
capture_count = 0;
|
||||
while (frames && *frames && capture_count < MAX_CAPTURES)
|
||||
{
|
||||
captures[capture_count++] = atoi(frames);
|
||||
frames = strchr(frames, ',');
|
||||
if (frames)
|
||||
frames++;
|
||||
}
|
||||
}
|
||||
|
||||
int test_plan_captures_frame(int frame)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < capture_count; i++)
|
||||
if (captures[i] == frame)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void test_plan_limit(int frames)
|
||||
{
|
||||
frame_limit = frames;
|
||||
}
|
||||
|
||||
int test_plan_finished(int frame)
|
||||
{
|
||||
return frame_limit && frame >= frame_limit;
|
||||
}
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "test_plan.h"
|
||||
|
||||
#define MAX_STEPS 128
|
||||
#define MAX_CAPTURES 64
|
||||
|
||||
typedef struct
|
||||
{
|
||||
int start, count;
|
||||
uint32_t buttons;
|
||||
int16_t lx, ly, rx, ry;
|
||||
} step_t;
|
||||
|
||||
static const struct
|
||||
{
|
||||
const char *name;
|
||||
uint32_t bit;
|
||||
} names[] = {
|
||||
{"UP", PAD_UP}, {"DOWN", PAD_DOWN}, {"LEFT", PAD_LEFT}, {"RIGHT", PAD_RIGHT},
|
||||
{"CROSS", PAD_CROSS}, {"CIRCLE", PAD_CIRCLE}, {"SQUARE", PAD_SQUARE},
|
||||
{"TRIANGLE", PAD_TRIANGLE}, {"L1", PAD_L1}, {"R1", PAD_R1}, {"L2", PAD_L2},
|
||||
{"R2", PAD_R2}, {"L3", PAD_L3}, {"R3", PAD_R3}, {"OPTIONS", PAD_OPTIONS},
|
||||
{"TOUCHPAD", PAD_TOUCHPAD},
|
||||
};
|
||||
|
||||
static step_t steps[MAX_STEPS];
|
||||
static int step_count;
|
||||
static int captures[MAX_CAPTURES];
|
||||
static int capture_count;
|
||||
static int frame_limit;
|
||||
|
||||
static void parse_token(step_t *step, const char *token)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < sizeof(names) / sizeof(names[0]); i++)
|
||||
if (!strcmp(token, names[i].name))
|
||||
step->buttons |= names[i].bit;
|
||||
if (!strncmp(token, "LX=", 3))
|
||||
step->lx = (int16_t)atoi(token + 3);
|
||||
if (!strncmp(token, "LY=", 3))
|
||||
step->ly = (int16_t)atoi(token + 3);
|
||||
if (!strncmp(token, "RX=", 3))
|
||||
step->rx = (int16_t)atoi(token + 3);
|
||||
if (!strncmp(token, "RY=", 3))
|
||||
step->ry = (int16_t)atoi(token + 3);
|
||||
}
|
||||
|
||||
void test_plan_input(const char *text)
|
||||
{
|
||||
char *copy, *entry, *save_entry;
|
||||
|
||||
step_count = 0;
|
||||
if (!text || !(copy = strdup(text)))
|
||||
return;
|
||||
for (entry = strtok_r(copy, ";", &save_entry); entry && step_count < MAX_STEPS;
|
||||
entry = strtok_r(NULL, ";", &save_entry))
|
||||
{
|
||||
step_t *step = &steps[step_count];
|
||||
char *field, *save_field;
|
||||
int index = 0;
|
||||
|
||||
memset(step, 0, sizeof(*step));
|
||||
for (field = strtok_r(entry, ":", &save_field); field;
|
||||
field = strtok_r(NULL, ":", &save_field), index++)
|
||||
{
|
||||
if (index == 0)
|
||||
step->start = atoi(field);
|
||||
else if (index == 1)
|
||||
{
|
||||
char *token, *save_token;
|
||||
|
||||
for (token = strtok_r(field, "+", &save_token); token;
|
||||
token = strtok_r(NULL, "+", &save_token))
|
||||
parse_token(step, token);
|
||||
}
|
||||
else
|
||||
step->count = atoi(field);
|
||||
}
|
||||
if (!step->count)
|
||||
step->count = 1;
|
||||
step_count++;
|
||||
}
|
||||
free(copy);
|
||||
}
|
||||
|
||||
int test_plan_has_input(void)
|
||||
{
|
||||
return step_count > 0;
|
||||
}
|
||||
|
||||
void test_plan_apply(int frame, pad_state_t *state)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < step_count; i++)
|
||||
{
|
||||
const step_t *step = &steps[i];
|
||||
|
||||
if (frame < step->start || frame >= step->start + step->count)
|
||||
continue;
|
||||
state->buttons |= step->buttons;
|
||||
if (step->lx)
|
||||
state->lx = step->lx;
|
||||
if (step->ly)
|
||||
state->ly = step->ly;
|
||||
if (step->rx)
|
||||
state->rx = step->rx;
|
||||
if (step->ry)
|
||||
state->ry = step->ry;
|
||||
}
|
||||
}
|
||||
|
||||
void test_plan_captures(const char *frames)
|
||||
{
|
||||
capture_count = 0;
|
||||
while (frames && *frames && capture_count < MAX_CAPTURES)
|
||||
{
|
||||
captures[capture_count++] = atoi(frames);
|
||||
frames = strchr(frames, ',');
|
||||
if (frames)
|
||||
frames++;
|
||||
}
|
||||
}
|
||||
|
||||
int test_plan_captures_frame(int frame)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < capture_count; i++)
|
||||
if (captures[i] == frame)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void test_plan_limit(int frames)
|
||||
{
|
||||
frame_limit = frames;
|
||||
}
|
||||
|
||||
int test_plan_finished(int frame)
|
||||
{
|
||||
return frame_limit && frame >= frame_limit;
|
||||
}
|
||||
+14
-14
@@ -1,14 +1,14 @@
|
||||
#ifndef TEST_PLAN_H
|
||||
#define TEST_PLAN_H
|
||||
|
||||
#include "platform.h"
|
||||
|
||||
void test_plan_input(const char *steps);
|
||||
void test_plan_captures(const char *frames);
|
||||
int test_plan_has_input(void);
|
||||
void test_plan_apply(int frame, pad_state_t *state);
|
||||
int test_plan_captures_frame(int frame);
|
||||
void test_plan_limit(int frames);
|
||||
int test_plan_finished(int frame);
|
||||
|
||||
#endif
|
||||
#ifndef TEST_PLAN_H
|
||||
#define TEST_PLAN_H
|
||||
|
||||
#include "platform.h"
|
||||
|
||||
void test_plan_input(const char *steps);
|
||||
void test_plan_captures(const char *frames);
|
||||
int test_plan_has_input(void);
|
||||
void test_plan_apply(int frame, pad_state_t *state);
|
||||
int test_plan_captures_frame(int frame);
|
||||
void test_plan_limit(int frames);
|
||||
int test_plan_finished(int frame);
|
||||
|
||||
#endif
|
||||
+169
-169
@@ -1,169 +1,169 @@
|
||||
#include <stdio.h>
|
||||
|
||||
#include "doomdef.h"
|
||||
#include "i_sound.h"
|
||||
#include "sounds.h"
|
||||
#include "w_wad.h"
|
||||
#include "z_zone.h"
|
||||
|
||||
#include "audio.h"
|
||||
|
||||
#define SFX_HEADER 8
|
||||
#define SFX_PADDING 16
|
||||
#define MUSIC_HANDLE 1
|
||||
|
||||
typedef struct
|
||||
{
|
||||
const byte *samples;
|
||||
int length;
|
||||
int rate;
|
||||
} sample_t;
|
||||
|
||||
static sample_t samples[NUMSFX];
|
||||
static int sound_ready;
|
||||
|
||||
static void cache_sample(int id)
|
||||
{
|
||||
const byte *lump;
|
||||
int size, length;
|
||||
|
||||
S_sfx[id].lumpnum = I_GetSfxLumpNum(&S_sfx[id]);
|
||||
lump = W_CacheLumpNum(S_sfx[id].lumpnum, PU_STATIC);
|
||||
size = W_LumpLength(S_sfx[id].lumpnum);
|
||||
S_sfx[id].data = (void *)lump;
|
||||
if (size < SFX_HEADER)
|
||||
return;
|
||||
|
||||
length = lump[4] | lump[5] << 8 | lump[6] << 16 | lump[7] << 24;
|
||||
if (length > size - SFX_HEADER)
|
||||
length = size - SFX_HEADER;
|
||||
samples[id].samples = lump + SFX_HEADER;
|
||||
samples[id].length = length;
|
||||
samples[id].rate = lump[2] | lump[3] << 8;
|
||||
if (length > 2 * SFX_PADDING)
|
||||
{
|
||||
samples[id].samples += SFX_PADDING;
|
||||
samples[id].length -= 2 * SFX_PADDING;
|
||||
}
|
||||
}
|
||||
|
||||
static const sample_t *sample_for(int id)
|
||||
{
|
||||
sfxinfo_t *sfx = &S_sfx[id];
|
||||
|
||||
while (sfx->link)
|
||||
sfx = sfx->link;
|
||||
return &samples[sfx - S_sfx];
|
||||
}
|
||||
|
||||
void I_InitSound(void)
|
||||
{
|
||||
int lump, id;
|
||||
|
||||
for (id = 1; id < NUMSFX; id++)
|
||||
if (!S_sfx[id].link)
|
||||
cache_sample(id);
|
||||
for (id = 1; id < NUMSFX; id++)
|
||||
if (S_sfx[id].link)
|
||||
S_sfx[id].data = S_sfx[id].link->data;
|
||||
|
||||
lump = W_CheckNumForName("GENMIDI");
|
||||
sound_ready = audio_init(lump >= 0 ? W_CacheLumpNum(lump, PU_STATIC) : NULL,
|
||||
lump >= 0 ? W_LumpLength(lump) : 0) == 0;
|
||||
}
|
||||
|
||||
void I_ShutdownSound(void)
|
||||
{
|
||||
if (sound_ready)
|
||||
audio_shutdown();
|
||||
sound_ready = 0;
|
||||
}
|
||||
|
||||
void I_SetChannels(void)
|
||||
{
|
||||
}
|
||||
|
||||
int I_GetSfxLumpNum(sfxinfo_t *sfx)
|
||||
{
|
||||
char name[16];
|
||||
|
||||
snprintf(name, sizeof(name), "ds%s", sfx->name);
|
||||
if (W_CheckNumForName(name) < 0)
|
||||
return W_GetNumForName("dspistol");
|
||||
return W_GetNumForName(name);
|
||||
}
|
||||
|
||||
int I_StartSound(int id, int vol, int sep, int pitch, int priority)
|
||||
{
|
||||
const sample_t *sample = sample_for(id);
|
||||
|
||||
(void)priority;
|
||||
if (!sound_ready || !sample->samples)
|
||||
return -1;
|
||||
return audio_sfx_start(sample->samples, sample->length, sample->rate,
|
||||
vol * 127 / 15, sep, pitch);
|
||||
}
|
||||
|
||||
void I_StopSound(int handle)
|
||||
{
|
||||
audio_sfx_stop(handle);
|
||||
}
|
||||
|
||||
int I_SoundIsPlaying(int handle)
|
||||
{
|
||||
return audio_sfx_playing(handle);
|
||||
}
|
||||
|
||||
void I_UpdateSoundParams(int handle, int vol, int sep, int pitch)
|
||||
{
|
||||
audio_sfx_update(handle, vol * 127 / 15, sep, pitch);
|
||||
}
|
||||
|
||||
void I_InitMusic(void)
|
||||
{
|
||||
}
|
||||
|
||||
void I_ShutdownMusic(void)
|
||||
{
|
||||
audio_music_stop();
|
||||
}
|
||||
|
||||
void I_SetMusicVolume(int volume)
|
||||
{
|
||||
audio_music_volume(volume * 127 / 15);
|
||||
}
|
||||
|
||||
void I_PauseSong(int handle)
|
||||
{
|
||||
(void)handle;
|
||||
audio_music_pause(1);
|
||||
}
|
||||
|
||||
void I_ResumeSong(int handle)
|
||||
{
|
||||
(void)handle;
|
||||
audio_music_pause(0);
|
||||
}
|
||||
|
||||
int I_RegisterSong(void *data)
|
||||
{
|
||||
return audio_music_load(data) == 0 ? MUSIC_HANDLE : 0;
|
||||
}
|
||||
|
||||
void I_PlaySong(int handle, int looping)
|
||||
{
|
||||
if (handle == MUSIC_HANDLE)
|
||||
audio_music_play(looping);
|
||||
}
|
||||
|
||||
void I_StopSong(int handle)
|
||||
{
|
||||
(void)handle;
|
||||
audio_music_stop();
|
||||
}
|
||||
|
||||
void I_UnRegisterSong(int handle)
|
||||
{
|
||||
(void)handle;
|
||||
audio_music_stop();
|
||||
}
|
||||
#include <stdio.h>
|
||||
|
||||
#include "doomdef.h"
|
||||
#include "i_sound.h"
|
||||
#include "sounds.h"
|
||||
#include "w_wad.h"
|
||||
#include "z_zone.h"
|
||||
|
||||
#include "audio.h"
|
||||
|
||||
#define SFX_HEADER 8
|
||||
#define SFX_PADDING 16
|
||||
#define MUSIC_HANDLE 1
|
||||
|
||||
typedef struct
|
||||
{
|
||||
const byte *samples;
|
||||
int length;
|
||||
int rate;
|
||||
} sample_t;
|
||||
|
||||
static sample_t samples[NUMSFX];
|
||||
static int sound_ready;
|
||||
|
||||
static void cache_sample(int id)
|
||||
{
|
||||
const byte *lump;
|
||||
int size, length;
|
||||
|
||||
S_sfx[id].lumpnum = I_GetSfxLumpNum(&S_sfx[id]);
|
||||
lump = W_CacheLumpNum(S_sfx[id].lumpnum, PU_STATIC);
|
||||
size = W_LumpLength(S_sfx[id].lumpnum);
|
||||
S_sfx[id].data = (void *)lump;
|
||||
if (size < SFX_HEADER)
|
||||
return;
|
||||
|
||||
length = lump[4] | lump[5] << 8 | lump[6] << 16 | lump[7] << 24;
|
||||
if (length > size - SFX_HEADER)
|
||||
length = size - SFX_HEADER;
|
||||
samples[id].samples = lump + SFX_HEADER;
|
||||
samples[id].length = length;
|
||||
samples[id].rate = lump[2] | lump[3] << 8;
|
||||
if (length > 2 * SFX_PADDING)
|
||||
{
|
||||
samples[id].samples += SFX_PADDING;
|
||||
samples[id].length -= 2 * SFX_PADDING;
|
||||
}
|
||||
}
|
||||
|
||||
static const sample_t *sample_for(int id)
|
||||
{
|
||||
sfxinfo_t *sfx = &S_sfx[id];
|
||||
|
||||
while (sfx->link)
|
||||
sfx = sfx->link;
|
||||
return &samples[sfx - S_sfx];
|
||||
}
|
||||
|
||||
void I_InitSound(void)
|
||||
{
|
||||
int lump, id;
|
||||
|
||||
for (id = 1; id < NUMSFX; id++)
|
||||
if (!S_sfx[id].link)
|
||||
cache_sample(id);
|
||||
for (id = 1; id < NUMSFX; id++)
|
||||
if (S_sfx[id].link)
|
||||
S_sfx[id].data = S_sfx[id].link->data;
|
||||
|
||||
lump = W_CheckNumForName("GENMIDI");
|
||||
sound_ready = audio_init(lump >= 0 ? W_CacheLumpNum(lump, PU_STATIC) : NULL,
|
||||
lump >= 0 ? W_LumpLength(lump) : 0) == 0;
|
||||
}
|
||||
|
||||
void I_ShutdownSound(void)
|
||||
{
|
||||
if (sound_ready)
|
||||
audio_shutdown();
|
||||
sound_ready = 0;
|
||||
}
|
||||
|
||||
void I_SetChannels(void)
|
||||
{
|
||||
}
|
||||
|
||||
int I_GetSfxLumpNum(sfxinfo_t *sfx)
|
||||
{
|
||||
char name[16];
|
||||
|
||||
snprintf(name, sizeof(name), "ds%s", sfx->name);
|
||||
if (W_CheckNumForName(name) < 0)
|
||||
return W_GetNumForName("dspistol");
|
||||
return W_GetNumForName(name);
|
||||
}
|
||||
|
||||
int I_StartSound(int id, int vol, int sep, int pitch, int priority)
|
||||
{
|
||||
const sample_t *sample = sample_for(id);
|
||||
|
||||
(void)priority;
|
||||
if (!sound_ready || !sample->samples)
|
||||
return -1;
|
||||
return audio_sfx_start(sample->samples, sample->length, sample->rate,
|
||||
vol * 127 / 15, sep, pitch);
|
||||
}
|
||||
|
||||
void I_StopSound(int handle)
|
||||
{
|
||||
audio_sfx_stop(handle);
|
||||
}
|
||||
|
||||
int I_SoundIsPlaying(int handle)
|
||||
{
|
||||
return audio_sfx_playing(handle);
|
||||
}
|
||||
|
||||
void I_UpdateSoundParams(int handle, int vol, int sep, int pitch)
|
||||
{
|
||||
audio_sfx_update(handle, vol * 127 / 15, sep, pitch);
|
||||
}
|
||||
|
||||
void I_InitMusic(void)
|
||||
{
|
||||
}
|
||||
|
||||
void I_ShutdownMusic(void)
|
||||
{
|
||||
audio_music_stop();
|
||||
}
|
||||
|
||||
void I_SetMusicVolume(int volume)
|
||||
{
|
||||
audio_music_volume(volume * 127 / 15);
|
||||
}
|
||||
|
||||
void I_PauseSong(int handle)
|
||||
{
|
||||
(void)handle;
|
||||
audio_music_pause(1);
|
||||
}
|
||||
|
||||
void I_ResumeSong(int handle)
|
||||
{
|
||||
(void)handle;
|
||||
audio_music_pause(0);
|
||||
}
|
||||
|
||||
int I_RegisterSong(void *data)
|
||||
{
|
||||
return audio_music_load(data) == 0 ? MUSIC_HANDLE : 0;
|
||||
}
|
||||
|
||||
void I_PlaySong(int handle, int looping)
|
||||
{
|
||||
if (handle == MUSIC_HANDLE)
|
||||
audio_music_play(looping);
|
||||
}
|
||||
|
||||
void I_StopSong(int handle)
|
||||
{
|
||||
(void)handle;
|
||||
audio_music_stop();
|
||||
}
|
||||
|
||||
void I_UnRegisterSong(int handle)
|
||||
{
|
||||
(void)handle;
|
||||
audio_music_stop();
|
||||
}
|
||||
+63
-63
@@ -1,63 +1,63 @@
|
||||
#include <pthread.h>
|
||||
|
||||
#include "platform.h"
|
||||
#include "ps5.h"
|
||||
#include "sce.h"
|
||||
|
||||
#define GRAIN 256
|
||||
|
||||
static int port = -1;
|
||||
static pthread_t thread;
|
||||
static volatile int running;
|
||||
static audio_render_fn render;
|
||||
static void *render_user;
|
||||
|
||||
static void *output_loop(void *arg)
|
||||
{
|
||||
int16_t block[GRAIN * 2];
|
||||
|
||||
(void)arg;
|
||||
while (running)
|
||||
{
|
||||
render(block, GRAIN, render_user);
|
||||
sceAudioOutOutput(port, block);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int plat_audio_init(int rate, audio_render_fn fn, void *user)
|
||||
{
|
||||
sceAudioOutInit();
|
||||
port = sceAudioOutOpen(ps5_user(), SCE_AUDIO_PORT_MAIN, 0, GRAIN, rate, SCE_AUDIO_S16_STEREO);
|
||||
if (port < 0)
|
||||
port = sceAudioOutOpen(SCE_USER_SYSTEM, SCE_AUDIO_PORT_MAIN, 0, GRAIN, rate,
|
||||
SCE_AUDIO_S16_STEREO);
|
||||
if (port < 0)
|
||||
{
|
||||
plat_log("ps5: audio open failed %#x\n", port);
|
||||
return -1;
|
||||
}
|
||||
|
||||
render = fn;
|
||||
render_user = user;
|
||||
running = 1;
|
||||
plat_log("audio: output open at %d Hz\n", rate);
|
||||
if (pthread_create(&thread, NULL, output_loop, NULL))
|
||||
{
|
||||
running = 0;
|
||||
sceAudioOutClose(port);
|
||||
port = -1;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void plat_audio_shutdown(void)
|
||||
{
|
||||
if (!running)
|
||||
return;
|
||||
running = 0;
|
||||
pthread_join(thread, NULL);
|
||||
sceAudioOutClose(port);
|
||||
port = -1;
|
||||
}
|
||||
#include <pthread.h>
|
||||
|
||||
#include "platform.h"
|
||||
#include "ps5.h"
|
||||
#include "sce.h"
|
||||
|
||||
#define GRAIN 256
|
||||
|
||||
static int port = -1;
|
||||
static pthread_t thread;
|
||||
static volatile int running;
|
||||
static audio_render_fn render;
|
||||
static void *render_user;
|
||||
|
||||
static void *output_loop(void *arg)
|
||||
{
|
||||
int16_t block[GRAIN * 2];
|
||||
|
||||
(void)arg;
|
||||
while (running)
|
||||
{
|
||||
render(block, GRAIN, render_user);
|
||||
sceAudioOutOutput(port, block);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int plat_audio_init(int rate, audio_render_fn fn, void *user)
|
||||
{
|
||||
sceAudioOutInit();
|
||||
port = sceAudioOutOpen(ps5_user(), SCE_AUDIO_PORT_MAIN, 0, GRAIN, rate, SCE_AUDIO_S16_STEREO);
|
||||
if (port < 0)
|
||||
port = sceAudioOutOpen(SCE_USER_SYSTEM, SCE_AUDIO_PORT_MAIN, 0, GRAIN, rate,
|
||||
SCE_AUDIO_S16_STEREO);
|
||||
if (port < 0)
|
||||
{
|
||||
plat_log("ps5: audio open failed %#x\n", port);
|
||||
return -1;
|
||||
}
|
||||
|
||||
render = fn;
|
||||
render_user = user;
|
||||
running = 1;
|
||||
plat_log("audio: output open at %d Hz\n", rate);
|
||||
if (pthread_create(&thread, NULL, output_loop, NULL))
|
||||
{
|
||||
running = 0;
|
||||
sceAudioOutClose(port);
|
||||
port = -1;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void plat_audio_shutdown(void)
|
||||
{
|
||||
if (!running)
|
||||
return;
|
||||
running = 0;
|
||||
pthread_join(thread, NULL);
|
||||
sceAudioOutClose(port);
|
||||
port = -1;
|
||||
}
|
||||
+146
-146
@@ -1,146 +1,146 @@
|
||||
#include <netinet/in.h>
|
||||
#include <pthread.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "platform.h"
|
||||
#include "ps5.h"
|
||||
#include "sce.h"
|
||||
|
||||
#define MAX_QUEUED 16
|
||||
#define DRAIN_STEP_US 100000
|
||||
|
||||
typedef struct
|
||||
{
|
||||
char name[32];
|
||||
void *data;
|
||||
size_t size;
|
||||
} item_t;
|
||||
|
||||
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static item_t queue[MAX_QUEUED];
|
||||
static int queued;
|
||||
static int listener = -1;
|
||||
|
||||
static int send_all(int fd, const void *data, size_t size)
|
||||
{
|
||||
const uint8_t *p = data;
|
||||
|
||||
while (size)
|
||||
{
|
||||
ssize_t sent = send(fd, p, size, 0);
|
||||
|
||||
if (sent <= 0)
|
||||
return -1;
|
||||
p += sent;
|
||||
size -= (size_t)sent;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int send_item(int fd, const item_t *item)
|
||||
{
|
||||
uint32_t name_length = (uint32_t)strlen(item->name);
|
||||
uint64_t size = item->size;
|
||||
|
||||
return send_all(fd, &name_length, sizeof(name_length)) ||
|
||||
send_all(fd, item->name, name_length) || send_all(fd, &size, sizeof(size)) ||
|
||||
send_all(fd, item->data, item->size);
|
||||
}
|
||||
|
||||
static void *serve(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
for (;;)
|
||||
{
|
||||
int client = accept(listener, NULL, NULL);
|
||||
uint32_t end = 0;
|
||||
|
||||
if (client < 0)
|
||||
continue;
|
||||
pthread_mutex_lock(&lock);
|
||||
while (queued && send_item(client, &queue[0]) == 0)
|
||||
{
|
||||
munmap(queue[0].data, queue[0].size);
|
||||
memmove(queue, queue + 1, (size_t)--queued * sizeof(*queue));
|
||||
}
|
||||
pthread_mutex_unlock(&lock);
|
||||
send_all(client, &end, sizeof(end));
|
||||
close(client);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void ps5_capture_serve(int port)
|
||||
{
|
||||
struct sockaddr_in address;
|
||||
pthread_t thread;
|
||||
int yes = 1;
|
||||
|
||||
listener = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (listener < 0)
|
||||
{
|
||||
plat_log("capture: no socket\n");
|
||||
return;
|
||||
}
|
||||
setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
|
||||
memset(&address, 0, sizeof(address));
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_port = htons((uint16_t)port);
|
||||
address.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
if (bind(listener, (struct sockaddr *)&address, sizeof(address)) || listen(listener, 1) ||
|
||||
pthread_create(&thread, NULL, serve, NULL))
|
||||
{
|
||||
plat_log("capture: cannot serve on port %d\n", port);
|
||||
close(listener);
|
||||
listener = -1;
|
||||
return;
|
||||
}
|
||||
plat_log("capture: serving on port %d\n", port);
|
||||
}
|
||||
|
||||
void ps5_capture_add(const char *name, const void *data, size_t size)
|
||||
{
|
||||
void *copy;
|
||||
|
||||
if (listener < 0)
|
||||
return;
|
||||
copy = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
|
||||
if (copy == MAP_FAILED)
|
||||
return;
|
||||
memcpy(copy, data, size);
|
||||
|
||||
pthread_mutex_lock(&lock);
|
||||
if (queued < MAX_QUEUED)
|
||||
{
|
||||
snprintf(queue[queued].name, sizeof(queue[queued].name), "%s", name);
|
||||
queue[queued].data = copy;
|
||||
queue[queued].size = size;
|
||||
queued++;
|
||||
copy = NULL;
|
||||
}
|
||||
pthread_mutex_unlock(&lock);
|
||||
if (copy)
|
||||
munmap(copy, size);
|
||||
}
|
||||
|
||||
void ps5_capture_drain(uint32_t timeout_us)
|
||||
{
|
||||
uint32_t waited = 0;
|
||||
int pending = 1;
|
||||
|
||||
while (listener >= 0 && waited < timeout_us)
|
||||
{
|
||||
pthread_mutex_lock(&lock);
|
||||
pending = queued;
|
||||
pthread_mutex_unlock(&lock);
|
||||
if (!pending)
|
||||
return;
|
||||
sceKernelUsleep(DRAIN_STEP_US);
|
||||
waited += DRAIN_STEP_US;
|
||||
}
|
||||
}
|
||||
#include <netinet/in.h>
|
||||
#include <pthread.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "platform.h"
|
||||
#include "ps5.h"
|
||||
#include "sce.h"
|
||||
|
||||
#define MAX_QUEUED 16
|
||||
#define DRAIN_STEP_US 100000
|
||||
|
||||
typedef struct
|
||||
{
|
||||
char name[32];
|
||||
void *data;
|
||||
size_t size;
|
||||
} item_t;
|
||||
|
||||
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static item_t queue[MAX_QUEUED];
|
||||
static int queued;
|
||||
static int listener = -1;
|
||||
|
||||
static int send_all(int fd, const void *data, size_t size)
|
||||
{
|
||||
const uint8_t *p = data;
|
||||
|
||||
while (size)
|
||||
{
|
||||
ssize_t sent = send(fd, p, size, 0);
|
||||
|
||||
if (sent <= 0)
|
||||
return -1;
|
||||
p += sent;
|
||||
size -= (size_t)sent;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int send_item(int fd, const item_t *item)
|
||||
{
|
||||
uint32_t name_length = (uint32_t)strlen(item->name);
|
||||
uint64_t size = item->size;
|
||||
|
||||
return send_all(fd, &name_length, sizeof(name_length)) ||
|
||||
send_all(fd, item->name, name_length) || send_all(fd, &size, sizeof(size)) ||
|
||||
send_all(fd, item->data, item->size);
|
||||
}
|
||||
|
||||
static void *serve(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
for (;;)
|
||||
{
|
||||
int client = accept(listener, NULL, NULL);
|
||||
uint32_t end = 0;
|
||||
|
||||
if (client < 0)
|
||||
continue;
|
||||
pthread_mutex_lock(&lock);
|
||||
while (queued && send_item(client, &queue[0]) == 0)
|
||||
{
|
||||
munmap(queue[0].data, queue[0].size);
|
||||
memmove(queue, queue + 1, (size_t)--queued * sizeof(*queue));
|
||||
}
|
||||
pthread_mutex_unlock(&lock);
|
||||
send_all(client, &end, sizeof(end));
|
||||
close(client);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void ps5_capture_serve(int port)
|
||||
{
|
||||
struct sockaddr_in address;
|
||||
pthread_t thread;
|
||||
int yes = 1;
|
||||
|
||||
listener = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (listener < 0)
|
||||
{
|
||||
plat_log("capture: no socket\n");
|
||||
return;
|
||||
}
|
||||
setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
|
||||
memset(&address, 0, sizeof(address));
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_port = htons((uint16_t)port);
|
||||
address.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
if (bind(listener, (struct sockaddr *)&address, sizeof(address)) || listen(listener, 1) ||
|
||||
pthread_create(&thread, NULL, serve, NULL))
|
||||
{
|
||||
plat_log("capture: cannot serve on port %d\n", port);
|
||||
close(listener);
|
||||
listener = -1;
|
||||
return;
|
||||
}
|
||||
plat_log("capture: serving on port %d\n", port);
|
||||
}
|
||||
|
||||
void ps5_capture_add(const char *name, const void *data, size_t size)
|
||||
{
|
||||
void *copy;
|
||||
|
||||
if (listener < 0)
|
||||
return;
|
||||
copy = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
|
||||
if (copy == MAP_FAILED)
|
||||
return;
|
||||
memcpy(copy, data, size);
|
||||
|
||||
pthread_mutex_lock(&lock);
|
||||
if (queued < MAX_QUEUED)
|
||||
{
|
||||
snprintf(queue[queued].name, sizeof(queue[queued].name), "%s", name);
|
||||
queue[queued].data = copy;
|
||||
queue[queued].size = size;
|
||||
queued++;
|
||||
copy = NULL;
|
||||
}
|
||||
pthread_mutex_unlock(&lock);
|
||||
if (copy)
|
||||
munmap(copy, size);
|
||||
}
|
||||
|
||||
void ps5_capture_drain(uint32_t timeout_us)
|
||||
{
|
||||
uint32_t waited = 0;
|
||||
int pending = 1;
|
||||
|
||||
while (listener >= 0 && waited < timeout_us)
|
||||
{
|
||||
pthread_mutex_lock(&lock);
|
||||
pending = queued;
|
||||
pthread_mutex_unlock(&lock);
|
||||
if (!pending)
|
||||
return;
|
||||
sceKernelUsleep(DRAIN_STEP_US);
|
||||
waited += DRAIN_STEP_US;
|
||||
}
|
||||
}
|
||||
+21
-21
@@ -1,21 +1,21 @@
|
||||
#ifndef GPU_H
|
||||
#define GPU_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define GPU_TARGETS 2
|
||||
|
||||
typedef struct
|
||||
{
|
||||
int video;
|
||||
uint32_t *targets[GPU_TARGETS];
|
||||
int width, height, pitch;
|
||||
int view_x, view_width;
|
||||
int src_width, src_height;
|
||||
} gpu_config_t;
|
||||
|
||||
int gpu_init(const gpu_config_t *config);
|
||||
int gpu_present(const uint8_t *pixels, const uint32_t *rgba, int target, int64_t marker);
|
||||
int gpu_finish(void);
|
||||
|
||||
#endif
|
||||
#ifndef GPU_H
|
||||
#define GPU_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define GPU_TARGETS 2
|
||||
|
||||
typedef struct
|
||||
{
|
||||
int video;
|
||||
uint32_t *targets[GPU_TARGETS];
|
||||
int width, height, pitch;
|
||||
int view_x, view_width;
|
||||
int src_width, src_height;
|
||||
} gpu_config_t;
|
||||
|
||||
int gpu_init(const gpu_config_t *config);
|
||||
int gpu_present(const uint8_t *pixels, const uint32_t *rgba, int target, int64_t marker);
|
||||
int gpu_finish(void);
|
||||
|
||||
#endif
|
||||
+87
-87
@@ -1,87 +1,87 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "platform.h"
|
||||
#include "ps5.h"
|
||||
#include "sce.h"
|
||||
#include "test_plan.h"
|
||||
|
||||
static const struct
|
||||
{
|
||||
uint32_t sce;
|
||||
uint32_t pad;
|
||||
} button_map[] = {
|
||||
{SCE_PAD_UP, PAD_UP}, {SCE_PAD_DOWN, PAD_DOWN},
|
||||
{SCE_PAD_LEFT, PAD_LEFT}, {SCE_PAD_RIGHT, PAD_RIGHT},
|
||||
{SCE_PAD_CROSS, PAD_CROSS}, {SCE_PAD_CIRCLE, PAD_CIRCLE},
|
||||
{SCE_PAD_SQUARE, PAD_SQUARE}, {SCE_PAD_TRIANGLE, PAD_TRIANGLE},
|
||||
{SCE_PAD_L1, PAD_L1}, {SCE_PAD_R1, PAD_R1},
|
||||
{SCE_PAD_L2, PAD_L2}, {SCE_PAD_R2, PAD_R2},
|
||||
{SCE_PAD_L3, PAD_L3}, {SCE_PAD_R3, PAD_R3},
|
||||
{SCE_PAD_OPTIONS, PAD_OPTIONS}, {SCE_PAD_TOUCHPAD, PAD_TOUCHPAD},
|
||||
};
|
||||
|
||||
static int handle = -1;
|
||||
|
||||
void ps5_pad_open(void)
|
||||
{
|
||||
static const ScePadColor doom_red = {200, 0, 0, 255};
|
||||
|
||||
if (scePadInit() < 0)
|
||||
{
|
||||
plat_log("ps5: pad init failed\n");
|
||||
return;
|
||||
}
|
||||
handle = scePadOpen(ps5_user(), SCE_PAD_PORT_STANDARD, 0, NULL);
|
||||
if (handle < 0)
|
||||
{
|
||||
plat_log("ps5: pad open failed %#x\n", handle);
|
||||
return;
|
||||
}
|
||||
scePadSetVibrationMode(handle, 2);
|
||||
scePadSetLightBar(handle, &doom_red);
|
||||
plat_log("pad: open\n");
|
||||
}
|
||||
|
||||
void ps5_pad_close(void)
|
||||
{
|
||||
if (handle < 0)
|
||||
return;
|
||||
plat_pad_rumble(0, 0);
|
||||
scePadClose(handle);
|
||||
handle = -1;
|
||||
}
|
||||
|
||||
static int16_t stick(uint8_t raw)
|
||||
{
|
||||
return (int16_t)((raw - 128) * 256);
|
||||
}
|
||||
|
||||
int plat_pad_read(pad_state_t *state)
|
||||
{
|
||||
ScePadData data;
|
||||
int connected;
|
||||
size_t i;
|
||||
|
||||
memset(state, 0, sizeof(*state));
|
||||
connected = handle >= 0 && scePadReadState(handle, &data) >= 0 && data.connected;
|
||||
if (connected)
|
||||
{
|
||||
for (i = 0; i < sizeof(button_map) / sizeof(button_map[0]); i++)
|
||||
if (data.buttons & button_map[i].sce)
|
||||
state->buttons |= button_map[i].pad;
|
||||
state->lx = stick(data.lx);
|
||||
state->ly = stick(data.ly);
|
||||
state->rx = stick(data.rx);
|
||||
state->ry = stick(data.ry);
|
||||
}
|
||||
test_plan_apply(ps5_frame(), state);
|
||||
return connected || test_plan_has_input() ? 0 : -1;
|
||||
}
|
||||
|
||||
void plat_pad_rumble(uint8_t strong, uint8_t weak)
|
||||
{
|
||||
ScePadVibration vibration = {strong, weak};
|
||||
|
||||
if (handle >= 0)
|
||||
scePadSetVibration(handle, &vibration);
|
||||
}
|
||||
#include <string.h>
|
||||
|
||||
#include "platform.h"
|
||||
#include "ps5.h"
|
||||
#include "sce.h"
|
||||
#include "test_plan.h"
|
||||
|
||||
static const struct
|
||||
{
|
||||
uint32_t sce;
|
||||
uint32_t pad;
|
||||
} button_map[] = {
|
||||
{SCE_PAD_UP, PAD_UP}, {SCE_PAD_DOWN, PAD_DOWN},
|
||||
{SCE_PAD_LEFT, PAD_LEFT}, {SCE_PAD_RIGHT, PAD_RIGHT},
|
||||
{SCE_PAD_CROSS, PAD_CROSS}, {SCE_PAD_CIRCLE, PAD_CIRCLE},
|
||||
{SCE_PAD_SQUARE, PAD_SQUARE}, {SCE_PAD_TRIANGLE, PAD_TRIANGLE},
|
||||
{SCE_PAD_L1, PAD_L1}, {SCE_PAD_R1, PAD_R1},
|
||||
{SCE_PAD_L2, PAD_L2}, {SCE_PAD_R2, PAD_R2},
|
||||
{SCE_PAD_L3, PAD_L3}, {SCE_PAD_R3, PAD_R3},
|
||||
{SCE_PAD_OPTIONS, PAD_OPTIONS}, {SCE_PAD_TOUCHPAD, PAD_TOUCHPAD},
|
||||
};
|
||||
|
||||
static int handle = -1;
|
||||
|
||||
void ps5_pad_open(void)
|
||||
{
|
||||
static const ScePadColor doom_red = {200, 0, 0, 255};
|
||||
|
||||
if (scePadInit() < 0)
|
||||
{
|
||||
plat_log("ps5: pad init failed\n");
|
||||
return;
|
||||
}
|
||||
handle = scePadOpen(ps5_user(), SCE_PAD_PORT_STANDARD, 0, NULL);
|
||||
if (handle < 0)
|
||||
{
|
||||
plat_log("ps5: pad open failed %#x\n", handle);
|
||||
return;
|
||||
}
|
||||
scePadSetVibrationMode(handle, 2);
|
||||
scePadSetLightBar(handle, &doom_red);
|
||||
plat_log("pad: open\n");
|
||||
}
|
||||
|
||||
void ps5_pad_close(void)
|
||||
{
|
||||
if (handle < 0)
|
||||
return;
|
||||
plat_pad_rumble(0, 0);
|
||||
scePadClose(handle);
|
||||
handle = -1;
|
||||
}
|
||||
|
||||
static int16_t stick(uint8_t raw)
|
||||
{
|
||||
return (int16_t)((raw - 128) * 256);
|
||||
}
|
||||
|
||||
int plat_pad_read(pad_state_t *state)
|
||||
{
|
||||
ScePadData data;
|
||||
int connected;
|
||||
size_t i;
|
||||
|
||||
memset(state, 0, sizeof(*state));
|
||||
connected = handle >= 0 && scePadReadState(handle, &data) >= 0 && data.connected;
|
||||
if (connected)
|
||||
{
|
||||
for (i = 0; i < sizeof(button_map) / sizeof(button_map[0]); i++)
|
||||
if (data.buttons & button_map[i].sce)
|
||||
state->buttons |= button_map[i].pad;
|
||||
state->lx = stick(data.lx);
|
||||
state->ly = stick(data.ly);
|
||||
state->rx = stick(data.rx);
|
||||
state->ry = stick(data.ry);
|
||||
}
|
||||
test_plan_apply(ps5_frame(), state);
|
||||
return connected || test_plan_has_input() ? 0 : -1;
|
||||
}
|
||||
|
||||
void plat_pad_rumble(uint8_t strong, uint8_t weak)
|
||||
{
|
||||
ScePadVibration vibration = {strong, weak};
|
||||
|
||||
if (handle >= 0)
|
||||
scePadSetVibration(handle, &vibration);
|
||||
}
|
||||
+64
-64
@@ -1,64 +1,64 @@
|
||||
#include "tiling.h"
|
||||
|
||||
#define GROUP 64
|
||||
|
||||
static unsigned int blend(unsigned int a, unsigned int b, float t)
|
||||
{
|
||||
unsigned int out = 0;
|
||||
|
||||
for (int shift = 0; shift < 24; shift += 8)
|
||||
{
|
||||
float ca = (a >> shift) & 0xff, cb = (b >> shift) & 0xff;
|
||||
|
||||
out |= (unsigned int)(ca + (cb - ca) * t + 0.5f) << shift;
|
||||
}
|
||||
return out | 0xff000000u;
|
||||
}
|
||||
|
||||
static int clampi(int v, int lo, int hi)
|
||||
{
|
||||
return v < lo ? lo : v > hi ? hi : v;
|
||||
}
|
||||
|
||||
static float saturate(float v)
|
||||
{
|
||||
return __builtin_fminf(__builtin_fmaxf(v, 0.0f), 1.0f);
|
||||
}
|
||||
|
||||
__kernel __attribute__((reqd_work_group_size(GROUP, 1, 1))) void
|
||||
present(__global const unsigned char *src, __global const unsigned int *palette,
|
||||
__global unsigned int *dst, unsigned int pitch, unsigned int out_width,
|
||||
unsigned int out_height, unsigned int view_x, unsigned int view_width,
|
||||
unsigned int src_width, unsigned int src_height)
|
||||
{
|
||||
unsigned int y = __builtin_amdgcn_workgroup_id_x();
|
||||
float scale_x = (float)view_width / src_width;
|
||||
float scale_y = (float)out_height / src_height;
|
||||
float fy = (y + 0.5f) / scale_y - 0.5f;
|
||||
float iy = __builtin_floorf(fy);
|
||||
float ty = saturate((fy - iy - 0.5f) * scale_y + 0.5f);
|
||||
int y0 = clampi((int)iy, 0, src_height - 1), y1 = clampi((int)iy + 1, 0, src_height - 1);
|
||||
__global const unsigned char *row0 = src + y0 * src_width;
|
||||
__global const unsigned char *row1 = src + y1 * src_width;
|
||||
|
||||
if (y >= out_height)
|
||||
return;
|
||||
|
||||
for (unsigned int x = __builtin_amdgcn_workitem_id_x(); x < out_width; x += GROUP)
|
||||
{
|
||||
unsigned int color = 0xff000000u;
|
||||
|
||||
if (x >= view_x && x < view_x + view_width)
|
||||
{
|
||||
float fx = (x - view_x + 0.5f) / scale_x - 0.5f;
|
||||
float ix = __builtin_floorf(fx);
|
||||
float tx = saturate((fx - ix - 0.5f) * scale_x + 0.5f);
|
||||
int x0 = clampi((int)ix, 0, src_width - 1), x1 = clampi((int)ix + 1, 0, src_width - 1);
|
||||
unsigned int top = blend(palette[row0[x0]], palette[row0[x1]], tx);
|
||||
unsigned int bottom = blend(palette[row1[x0]], palette[row1[x1]], tx);
|
||||
|
||||
color = blend(top, bottom, ty);
|
||||
}
|
||||
dst[tiled_index(x, y, pitch)] = color;
|
||||
}
|
||||
}
|
||||
#include "tiling.h"
|
||||
|
||||
#define GROUP 64
|
||||
|
||||
static unsigned int blend(unsigned int a, unsigned int b, float t)
|
||||
{
|
||||
unsigned int out = 0;
|
||||
|
||||
for (int shift = 0; shift < 24; shift += 8)
|
||||
{
|
||||
float ca = (a >> shift) & 0xff, cb = (b >> shift) & 0xff;
|
||||
|
||||
out |= (unsigned int)(ca + (cb - ca) * t + 0.5f) << shift;
|
||||
}
|
||||
return out | 0xff000000u;
|
||||
}
|
||||
|
||||
static int clampi(int v, int lo, int hi)
|
||||
{
|
||||
return v < lo ? lo : v > hi ? hi : v;
|
||||
}
|
||||
|
||||
static float saturate(float v)
|
||||
{
|
||||
return __builtin_fminf(__builtin_fmaxf(v, 0.0f), 1.0f);
|
||||
}
|
||||
|
||||
__kernel __attribute__((reqd_work_group_size(GROUP, 1, 1))) void
|
||||
present(__global const unsigned char *src, __global const unsigned int *palette,
|
||||
__global unsigned int *dst, unsigned int pitch, unsigned int out_width,
|
||||
unsigned int out_height, unsigned int view_x, unsigned int view_width,
|
||||
unsigned int src_width, unsigned int src_height)
|
||||
{
|
||||
unsigned int y = __builtin_amdgcn_workgroup_id_x();
|
||||
float scale_x = (float)view_width / src_width;
|
||||
float scale_y = (float)out_height / src_height;
|
||||
float fy = (y + 0.5f) / scale_y - 0.5f;
|
||||
float iy = __builtin_floorf(fy);
|
||||
float ty = saturate((fy - iy - 0.5f) * scale_y + 0.5f);
|
||||
int y0 = clampi((int)iy, 0, src_height - 1), y1 = clampi((int)iy + 1, 0, src_height - 1);
|
||||
__global const unsigned char *row0 = src + y0 * src_width;
|
||||
__global const unsigned char *row1 = src + y1 * src_width;
|
||||
|
||||
if (y >= out_height)
|
||||
return;
|
||||
|
||||
for (unsigned int x = __builtin_amdgcn_workitem_id_x(); x < out_width; x += GROUP)
|
||||
{
|
||||
unsigned int color = 0xff000000u;
|
||||
|
||||
if (x >= view_x && x < view_x + view_width)
|
||||
{
|
||||
float fx = (x - view_x + 0.5f) / scale_x - 0.5f;
|
||||
float ix = __builtin_floorf(fx);
|
||||
float tx = saturate((fx - ix - 0.5f) * scale_x + 0.5f);
|
||||
int x0 = clampi((int)ix, 0, src_width - 1), x1 = clampi((int)ix + 1, 0, src_width - 1);
|
||||
unsigned int top = blend(palette[row0[x0]], palette[row0[x1]], tx);
|
||||
unsigned int bottom = blend(palette[row1[x0]], palette[row1[x1]], tx);
|
||||
|
||||
color = blend(top, bottom, ty);
|
||||
}
|
||||
dst[tiled_index(x, y, pitch)] = color;
|
||||
}
|
||||
}
|
||||
+18
-18
@@ -1,18 +1,18 @@
|
||||
#ifndef PS5_H
|
||||
#define PS5_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
int ps5_user(void);
|
||||
int ps5_frame(void);
|
||||
int ps5_test_cpu_present(void);
|
||||
void *ps5_gpu_memory(size_t bytes);
|
||||
void ps5_cache_flush(const volatile void *address, size_t bytes);
|
||||
void ps5_pad_open(void);
|
||||
void ps5_pad_close(void);
|
||||
void ps5_capture_serve(int port);
|
||||
void ps5_capture_add(const char *name, const void *data, size_t size);
|
||||
void ps5_capture_drain(uint32_t timeout_us);
|
||||
|
||||
#endif
|
||||
#ifndef PS5_H
|
||||
#define PS5_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
int ps5_user(void);
|
||||
int ps5_frame(void);
|
||||
int ps5_test_cpu_present(void);
|
||||
void *ps5_gpu_memory(size_t bytes);
|
||||
void ps5_cache_flush(const volatile void *address, size_t bytes);
|
||||
void ps5_pad_open(void);
|
||||
void ps5_pad_close(void);
|
||||
void ps5_capture_serve(int port);
|
||||
void ps5_capture_add(const char *name, const void *data, size_t size);
|
||||
void ps5_capture_drain(uint32_t timeout_us);
|
||||
|
||||
#endif
|
||||
+192
-192
@@ -1,192 +1,192 @@
|
||||
#ifndef SCE_H
|
||||
#define SCE_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define SCE_USER_SYSTEM 0xff
|
||||
|
||||
#define SCE_MEMORY_GPU_SHARED 12
|
||||
#define SCE_PROT_CPU_GPU_RW 0x33
|
||||
#define SCE_MEMORY_ALIGN 0x200000
|
||||
|
||||
#define SCE_VIDEO_BUS_MAIN 0
|
||||
#define SCE_VIDEO_FLIP_VSYNC 1
|
||||
#define SCE_VIDEO_FORMAT_RGBA8_SRGB UINT64_C(0x8000000022000000)
|
||||
#define SCE_VIDEO_TILING_TILED 0
|
||||
|
||||
#define SCE_PAD_PORT_STANDARD 0
|
||||
#define SCE_PAD_L3 0x0002
|
||||
#define SCE_PAD_R3 0x0004
|
||||
#define SCE_PAD_OPTIONS 0x0008
|
||||
#define SCE_PAD_UP 0x0010
|
||||
#define SCE_PAD_RIGHT 0x0020
|
||||
#define SCE_PAD_DOWN 0x0040
|
||||
#define SCE_PAD_LEFT 0x0080
|
||||
#define SCE_PAD_L2 0x0100
|
||||
#define SCE_PAD_R2 0x0200
|
||||
#define SCE_PAD_L1 0x0400
|
||||
#define SCE_PAD_R1 0x0800
|
||||
#define SCE_PAD_TRIANGLE 0x1000
|
||||
#define SCE_PAD_CIRCLE 0x2000
|
||||
#define SCE_PAD_CROSS 0x4000
|
||||
#define SCE_PAD_SQUARE 0x8000
|
||||
#define SCE_PAD_TOUCHPAD 0x100000
|
||||
|
||||
#define SCE_AUDIO_PORT_MAIN 0
|
||||
#define SCE_AUDIO_S16_STEREO 1
|
||||
|
||||
typedef struct
|
||||
{
|
||||
void *data;
|
||||
void *metadata;
|
||||
void *reserved[2];
|
||||
} SceVideoOutBuffer;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint8_t opaque[80];
|
||||
} SceVideoOutAttribute;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint64_t count;
|
||||
uint64_t process_time;
|
||||
uint64_t tsc;
|
||||
int64_t flip_arg;
|
||||
uint8_t reserved[96];
|
||||
} SceVideoOutFlipStatus;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint16_t x, y;
|
||||
uint8_t finger;
|
||||
uint8_t reserved[3];
|
||||
} ScePadTouch;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint8_t fingers;
|
||||
uint8_t reserved1[3];
|
||||
uint32_t reserved2;
|
||||
ScePadTouch touch[2];
|
||||
} ScePadTouchData;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint32_t buttons;
|
||||
uint8_t lx, ly, rx, ry;
|
||||
uint8_t l2, r2;
|
||||
uint16_t reserved;
|
||||
float orientation[4];
|
||||
float velocity[3];
|
||||
float acceleration[3];
|
||||
ScePadTouchData touch;
|
||||
uint8_t connected;
|
||||
uint64_t timestamp;
|
||||
uint8_t extension[16];
|
||||
uint8_t count;
|
||||
uint8_t reserved2[15];
|
||||
} ScePadData;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint8_t large_motor;
|
||||
uint8_t small_motor;
|
||||
} ScePadVibration;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint8_t r, g, b, a;
|
||||
} ScePadColor;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
char reserved[45];
|
||||
char message[3075];
|
||||
} SceNotificationRequest;
|
||||
|
||||
int64_t sceKernelGetDirectMemorySize(void);
|
||||
int sceKernelAllocateDirectMemory(int64_t start, int64_t end, size_t length, size_t alignment,
|
||||
int type, int64_t *physical);
|
||||
int sceKernelMapDirectMemory(void **address, size_t length, int protection, int flags,
|
||||
int64_t physical, size_t alignment);
|
||||
uint64_t sceKernelGetProcessTime(void);
|
||||
int sceKernelUsleep(uint32_t microseconds);
|
||||
int sceKernelSendNotificationRequest(int device, SceNotificationRequest *request, size_t size,
|
||||
int blocking);
|
||||
int sceKernelDebugOutText(int channel, const char *text);
|
||||
|
||||
int sceSystemServiceHideSplashScreen(void);
|
||||
int sceSystemServiceLoadExec(const char *path, char *const *argv);
|
||||
|
||||
int sceUserServiceInitialize(void *params);
|
||||
int sceUserServiceGetInitialUser(int *user);
|
||||
|
||||
int sceVideoOutOpen(int user, int bus, int index, const void *params);
|
||||
int sceVideoOutClose(int handle);
|
||||
int sceVideoOutSetFlipRate(int handle, int rate);
|
||||
void sceVideoOutSetBufferAttribute2(SceVideoOutAttribute *attribute, uint64_t format,
|
||||
uint32_t tiling, uint32_t width, uint32_t height,
|
||||
uint64_t option, uint32_t dcc_control, uint64_t dcc_clear);
|
||||
int sceVideoOutRegisterBuffers2(int handle, int set, int first, SceVideoOutBuffer *buffers,
|
||||
int count, SceVideoOutAttribute *attribute, int category,
|
||||
void *option);
|
||||
int sceVideoOutSubmitFlip(int handle, int index, uint32_t mode, int64_t argument);
|
||||
int sceVideoOutIsFlipPending(int handle);
|
||||
int sceVideoOutWaitVblank(int handle);
|
||||
int sceVideoOutGetFlipStatus(int handle, SceVideoOutFlipStatus *status);
|
||||
|
||||
int scePadInit(void);
|
||||
int scePadOpen(int user, int type, int index, const void *params);
|
||||
int scePadReadState(int handle, ScePadData *data);
|
||||
int scePadSetVibrationMode(int handle, int mode);
|
||||
int scePadSetVibration(int handle, const ScePadVibration *vibration);
|
||||
int scePadSetLightBar(int handle, const ScePadColor *color);
|
||||
int scePadClose(int handle);
|
||||
|
||||
typedef struct SceAgcCommandBuffer SceAgcCommandBuffer;
|
||||
typedef uint8_t (*SceAgcOutOfSpace)(SceAgcCommandBuffer *buffer, uint32_t words, void *user);
|
||||
|
||||
struct SceAgcCommandBuffer
|
||||
{
|
||||
uint32_t *bottom;
|
||||
uint32_t *top;
|
||||
uint32_t *up;
|
||||
uint32_t *down;
|
||||
SceAgcOutOfSpace out_of_space;
|
||||
void *user;
|
||||
uint32_t reserved_words;
|
||||
uint32_t padding;
|
||||
};
|
||||
|
||||
typedef struct
|
||||
{
|
||||
void *words;
|
||||
uint32_t word_count;
|
||||
uint8_t flags;
|
||||
uint8_t padding[3];
|
||||
} SceAgcSubmission;
|
||||
|
||||
int sceAgcInit(uint32_t version);
|
||||
uint32_t *sceAgcCbSetShRegisterRangeDirect(SceAgcCommandBuffer *cb, uint32_t offset,
|
||||
const uint32_t *values, uint32_t count);
|
||||
uint32_t *sceAgcCbDispatch(SceAgcCommandBuffer *cb, uint32_t x, uint32_t y, uint32_t z,
|
||||
uint32_t initiator);
|
||||
uint32_t *sceAgcDcbAcquireMem(SceAgcCommandBuffer *cb, uint8_t engine, uint32_t cb_db_op,
|
||||
uint32_t gcr_control, uint64_t base, uint64_t size,
|
||||
uint32_t poll_interval);
|
||||
uint32_t *sceAgcDcbSetFlip(SceAgcCommandBuffer *cb, uint32_t video, int index, uint32_t mode,
|
||||
int64_t argument);
|
||||
int sceAgcSuspendPoint(void);
|
||||
uint32_t sceAgcDriverGetWaitRenderingPacketSizeInDwords(void);
|
||||
uint32_t sceAgcDriverWaitUntilSafeForRendering(uint32_t **up, uint32_t words, uint32_t reserved,
|
||||
uint32_t video, int index);
|
||||
int sceAgcDriverSubmitDcb(SceAgcSubmission *submission);
|
||||
|
||||
int sceAudioOutInit(void);
|
||||
int sceAudioOutOpen(int user, int port, int index, uint32_t grain, uint32_t rate, uint32_t format);
|
||||
int sceAudioOutOutput(int handle, const void *samples);
|
||||
int sceAudioOutClose(int handle);
|
||||
|
||||
#endif
|
||||
#ifndef SCE_H
|
||||
#define SCE_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define SCE_USER_SYSTEM 0xff
|
||||
|
||||
#define SCE_MEMORY_GPU_SHARED 12
|
||||
#define SCE_PROT_CPU_GPU_RW 0x33
|
||||
#define SCE_MEMORY_ALIGN 0x200000
|
||||
|
||||
#define SCE_VIDEO_BUS_MAIN 0
|
||||
#define SCE_VIDEO_FLIP_VSYNC 1
|
||||
#define SCE_VIDEO_FORMAT_RGBA8_SRGB UINT64_C(0x8000000022000000)
|
||||
#define SCE_VIDEO_TILING_TILED 0
|
||||
|
||||
#define SCE_PAD_PORT_STANDARD 0
|
||||
#define SCE_PAD_L3 0x0002
|
||||
#define SCE_PAD_R3 0x0004
|
||||
#define SCE_PAD_OPTIONS 0x0008
|
||||
#define SCE_PAD_UP 0x0010
|
||||
#define SCE_PAD_RIGHT 0x0020
|
||||
#define SCE_PAD_DOWN 0x0040
|
||||
#define SCE_PAD_LEFT 0x0080
|
||||
#define SCE_PAD_L2 0x0100
|
||||
#define SCE_PAD_R2 0x0200
|
||||
#define SCE_PAD_L1 0x0400
|
||||
#define SCE_PAD_R1 0x0800
|
||||
#define SCE_PAD_TRIANGLE 0x1000
|
||||
#define SCE_PAD_CIRCLE 0x2000
|
||||
#define SCE_PAD_CROSS 0x4000
|
||||
#define SCE_PAD_SQUARE 0x8000
|
||||
#define SCE_PAD_TOUCHPAD 0x100000
|
||||
|
||||
#define SCE_AUDIO_PORT_MAIN 0
|
||||
#define SCE_AUDIO_S16_STEREO 1
|
||||
|
||||
typedef struct
|
||||
{
|
||||
void *data;
|
||||
void *metadata;
|
||||
void *reserved[2];
|
||||
} SceVideoOutBuffer;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint8_t opaque[80];
|
||||
} SceVideoOutAttribute;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint64_t count;
|
||||
uint64_t process_time;
|
||||
uint64_t tsc;
|
||||
int64_t flip_arg;
|
||||
uint8_t reserved[96];
|
||||
} SceVideoOutFlipStatus;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint16_t x, y;
|
||||
uint8_t finger;
|
||||
uint8_t reserved[3];
|
||||
} ScePadTouch;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint8_t fingers;
|
||||
uint8_t reserved1[3];
|
||||
uint32_t reserved2;
|
||||
ScePadTouch touch[2];
|
||||
} ScePadTouchData;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint32_t buttons;
|
||||
uint8_t lx, ly, rx, ry;
|
||||
uint8_t l2, r2;
|
||||
uint16_t reserved;
|
||||
float orientation[4];
|
||||
float velocity[3];
|
||||
float acceleration[3];
|
||||
ScePadTouchData touch;
|
||||
uint8_t connected;
|
||||
uint64_t timestamp;
|
||||
uint8_t extension[16];
|
||||
uint8_t count;
|
||||
uint8_t reserved2[15];
|
||||
} ScePadData;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint8_t large_motor;
|
||||
uint8_t small_motor;
|
||||
} ScePadVibration;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint8_t r, g, b, a;
|
||||
} ScePadColor;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
char reserved[45];
|
||||
char message[3075];
|
||||
} SceNotificationRequest;
|
||||
|
||||
int64_t sceKernelGetDirectMemorySize(void);
|
||||
int sceKernelAllocateDirectMemory(int64_t start, int64_t end, size_t length, size_t alignment,
|
||||
int type, int64_t *physical);
|
||||
int sceKernelMapDirectMemory(void **address, size_t length, int protection, int flags,
|
||||
int64_t physical, size_t alignment);
|
||||
uint64_t sceKernelGetProcessTime(void);
|
||||
int sceKernelUsleep(uint32_t microseconds);
|
||||
int sceKernelSendNotificationRequest(int device, SceNotificationRequest *request, size_t size,
|
||||
int blocking);
|
||||
int sceKernelDebugOutText(int channel, const char *text);
|
||||
|
||||
int sceSystemServiceHideSplashScreen(void);
|
||||
int sceSystemServiceLoadExec(const char *path, char *const *argv);
|
||||
|
||||
int sceUserServiceInitialize(void *params);
|
||||
int sceUserServiceGetInitialUser(int *user);
|
||||
|
||||
int sceVideoOutOpen(int user, int bus, int index, const void *params);
|
||||
int sceVideoOutClose(int handle);
|
||||
int sceVideoOutSetFlipRate(int handle, int rate);
|
||||
void sceVideoOutSetBufferAttribute2(SceVideoOutAttribute *attribute, uint64_t format,
|
||||
uint32_t tiling, uint32_t width, uint32_t height,
|
||||
uint64_t option, uint32_t dcc_control, uint64_t dcc_clear);
|
||||
int sceVideoOutRegisterBuffers2(int handle, int set, int first, SceVideoOutBuffer *buffers,
|
||||
int count, SceVideoOutAttribute *attribute, int category,
|
||||
void *option);
|
||||
int sceVideoOutSubmitFlip(int handle, int index, uint32_t mode, int64_t argument);
|
||||
int sceVideoOutIsFlipPending(int handle);
|
||||
int sceVideoOutWaitVblank(int handle);
|
||||
int sceVideoOutGetFlipStatus(int handle, SceVideoOutFlipStatus *status);
|
||||
|
||||
int scePadInit(void);
|
||||
int scePadOpen(int user, int type, int index, const void *params);
|
||||
int scePadReadState(int handle, ScePadData *data);
|
||||
int scePadSetVibrationMode(int handle, int mode);
|
||||
int scePadSetVibration(int handle, const ScePadVibration *vibration);
|
||||
int scePadSetLightBar(int handle, const ScePadColor *color);
|
||||
int scePadClose(int handle);
|
||||
|
||||
typedef struct SceAgcCommandBuffer SceAgcCommandBuffer;
|
||||
typedef uint8_t (*SceAgcOutOfSpace)(SceAgcCommandBuffer *buffer, uint32_t words, void *user);
|
||||
|
||||
struct SceAgcCommandBuffer
|
||||
{
|
||||
uint32_t *bottom;
|
||||
uint32_t *top;
|
||||
uint32_t *up;
|
||||
uint32_t *down;
|
||||
SceAgcOutOfSpace out_of_space;
|
||||
void *user;
|
||||
uint32_t reserved_words;
|
||||
uint32_t padding;
|
||||
};
|
||||
|
||||
typedef struct
|
||||
{
|
||||
void *words;
|
||||
uint32_t word_count;
|
||||
uint8_t flags;
|
||||
uint8_t padding[3];
|
||||
} SceAgcSubmission;
|
||||
|
||||
int sceAgcInit(uint32_t version);
|
||||
uint32_t *sceAgcCbSetShRegisterRangeDirect(SceAgcCommandBuffer *cb, uint32_t offset,
|
||||
const uint32_t *values, uint32_t count);
|
||||
uint32_t *sceAgcCbDispatch(SceAgcCommandBuffer *cb, uint32_t x, uint32_t y, uint32_t z,
|
||||
uint32_t initiator);
|
||||
uint32_t *sceAgcDcbAcquireMem(SceAgcCommandBuffer *cb, uint8_t engine, uint32_t cb_db_op,
|
||||
uint32_t gcr_control, uint64_t base, uint64_t size,
|
||||
uint32_t poll_interval);
|
||||
uint32_t *sceAgcDcbSetFlip(SceAgcCommandBuffer *cb, uint32_t video, int index, uint32_t mode,
|
||||
int64_t argument);
|
||||
int sceAgcSuspendPoint(void);
|
||||
uint32_t sceAgcDriverGetWaitRenderingPacketSizeInDwords(void);
|
||||
uint32_t sceAgcDriverWaitUntilSafeForRendering(uint32_t **up, uint32_t words, uint32_t reserved,
|
||||
uint32_t video, int index);
|
||||
int sceAgcDriverSubmitDcb(SceAgcSubmission *submission);
|
||||
|
||||
int sceAudioOutInit(void);
|
||||
int sceAudioOutOpen(int user, int port, int index, uint32_t grain, uint32_t rate, uint32_t format);
|
||||
int sceAudioOutOutput(int handle, const void *samples);
|
||||
int sceAudioOutClose(int handle);
|
||||
|
||||
#endif
|
||||
+202
-202
@@ -1,202 +1,202 @@
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "platform.h"
|
||||
#include "ps5.h"
|
||||
#include "sce.h"
|
||||
#include "test_plan.h"
|
||||
|
||||
#define SAVE_DIR "/download0"
|
||||
#define LOG_PATH SAVE_DIR "/doom.log"
|
||||
#define WAD_DIR "/app0/wads"
|
||||
#define TEST_PLAN_PATH "/app0/test.cfg"
|
||||
#define TEST_PLAN_BYTES 8192
|
||||
#define LOG_DRAIN_US 50000
|
||||
|
||||
static const char *const wad_dirs[] = {WAD_DIR, NULL};
|
||||
static int user = SCE_USER_SYSTEM;
|
||||
static int log_file = -1;
|
||||
static int test_cpu_present;
|
||||
|
||||
int ps5_user(void)
|
||||
{
|
||||
return user;
|
||||
}
|
||||
|
||||
static void log_text(const char *text, size_t length)
|
||||
{
|
||||
char line[512];
|
||||
size_t start = 0, i;
|
||||
|
||||
if (log_file >= 0)
|
||||
write(log_file, text, length);
|
||||
for (i = 0; i < length; i++)
|
||||
{
|
||||
if (text[i] != '\n' && i + 1 < length && i - start < sizeof(line) - 2)
|
||||
continue;
|
||||
memcpy(line, text + start, i - start + 1);
|
||||
line[i - start + 1] = 0;
|
||||
sceKernelDebugOutText(0, line);
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
static void *pump_output(void *arg)
|
||||
{
|
||||
int pipe_read = (int)(intptr_t)arg;
|
||||
char chunk[1024];
|
||||
ssize_t length;
|
||||
|
||||
while ((length = read(pipe_read, chunk, sizeof(chunk) - 1)) > 0)
|
||||
{
|
||||
chunk[length] = 0;
|
||||
log_text(chunk, (size_t)length);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void capture_output(void)
|
||||
{
|
||||
pthread_t thread;
|
||||
FILE *output;
|
||||
int fds[2];
|
||||
|
||||
log_file = open(LOG_PATH, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
if (log_file < 0)
|
||||
plat_log("ps5: cannot write %s (errno %d)\n", LOG_PATH, errno);
|
||||
if (pipe(fds))
|
||||
{
|
||||
plat_log("ps5: no output pipe (errno %d)\n", errno);
|
||||
return;
|
||||
}
|
||||
output = fdopen(fds[1], "w");
|
||||
if (!output)
|
||||
{
|
||||
plat_log("ps5: cannot redirect output (errno %d)\n", errno);
|
||||
return;
|
||||
}
|
||||
setvbuf(output, NULL, _IOLBF, 0);
|
||||
stdout = output;
|
||||
stderr = output;
|
||||
pthread_create(&thread, NULL, pump_output, (void *)(intptr_t)fds[0]);
|
||||
}
|
||||
|
||||
int ps5_test_cpu_present(void)
|
||||
{
|
||||
return test_cpu_present;
|
||||
}
|
||||
|
||||
static void load_test_plan(void)
|
||||
{
|
||||
static char text[TEST_PLAN_BYTES];
|
||||
char *line, *save;
|
||||
ssize_t length;
|
||||
int fd = open(TEST_PLAN_PATH, O_RDONLY);
|
||||
|
||||
if (fd < 0)
|
||||
return;
|
||||
length = read(fd, text, sizeof(text) - 1);
|
||||
close(fd);
|
||||
if (length <= 0)
|
||||
return;
|
||||
text[length] = 0;
|
||||
|
||||
for (line = strtok_r(text, "\r\n", &save); line; line = strtok_r(NULL, "\r\n", &save))
|
||||
{
|
||||
if (!strncmp(line, "input ", 6))
|
||||
test_plan_input(line + 6);
|
||||
else if (!strncmp(line, "capture ", 8))
|
||||
test_plan_captures(line + 8);
|
||||
else if (!strncmp(line, "frames ", 7))
|
||||
test_plan_limit(atoi(line + 7));
|
||||
else if (!strncmp(line, "serve ", 6))
|
||||
ps5_capture_serve(atoi(line + 6));
|
||||
else if (!strcmp(line, "present cpu"))
|
||||
test_cpu_present = 1;
|
||||
}
|
||||
plat_log("ps5: test plan loaded\n");
|
||||
}
|
||||
|
||||
int plat_init(int argc, char **argv)
|
||||
{
|
||||
int initial;
|
||||
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
capture_output();
|
||||
|
||||
if (sceUserServiceInitialize(NULL) < 0)
|
||||
plat_log("ps5: user service unavailable\n");
|
||||
else if (sceUserServiceGetInitialUser(&initial) == 0)
|
||||
user = initial;
|
||||
plat_log("ps5: user %d\n", user);
|
||||
load_test_plan();
|
||||
|
||||
ps5_pad_open();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void plat_shutdown(void)
|
||||
{
|
||||
ps5_pad_close();
|
||||
}
|
||||
|
||||
_Noreturn void plat_exit(int code)
|
||||
{
|
||||
plat_log("ps5: exit %d\n", code);
|
||||
fflush(NULL);
|
||||
sceKernelUsleep(LOG_DRAIN_US);
|
||||
sceSystemServiceLoadExec("exit", NULL);
|
||||
for (;;)
|
||||
sceKernelUsleep(1000000);
|
||||
}
|
||||
|
||||
void plat_log(const char *fmt, ...)
|
||||
{
|
||||
char line[512];
|
||||
va_list args;
|
||||
int length;
|
||||
|
||||
va_start(args, fmt);
|
||||
length = vsnprintf(line, sizeof(line), fmt, args);
|
||||
va_end(args);
|
||||
if (length < 0)
|
||||
return;
|
||||
log_text(line, (size_t)length < sizeof(line) ? (size_t)length : sizeof(line) - 1);
|
||||
}
|
||||
|
||||
void plat_alert(const char *message)
|
||||
{
|
||||
SceNotificationRequest request;
|
||||
|
||||
memset(&request, 0, sizeof(request));
|
||||
snprintf(request.message, sizeof(request.message), "DOOM: %s", message);
|
||||
sceKernelSendNotificationRequest(0, &request, sizeof(request), 0);
|
||||
}
|
||||
|
||||
uint64_t plat_ticks_us(void)
|
||||
{
|
||||
return sceKernelGetProcessTime();
|
||||
}
|
||||
|
||||
void plat_sleep_us(uint32_t us)
|
||||
{
|
||||
sceKernelUsleep(us);
|
||||
}
|
||||
|
||||
const char *const *plat_wad_dirs(void)
|
||||
{
|
||||
return wad_dirs;
|
||||
}
|
||||
|
||||
const char *plat_save_dir(void)
|
||||
{
|
||||
return SAVE_DIR;
|
||||
}
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "platform.h"
|
||||
#include "ps5.h"
|
||||
#include "sce.h"
|
||||
#include "test_plan.h"
|
||||
|
||||
#define SAVE_DIR "/download0"
|
||||
#define LOG_PATH SAVE_DIR "/doom.log"
|
||||
#define WAD_DIR "/app0/wads"
|
||||
#define TEST_PLAN_PATH "/app0/test.cfg"
|
||||
#define TEST_PLAN_BYTES 8192
|
||||
#define LOG_DRAIN_US 50000
|
||||
|
||||
static const char *const wad_dirs[] = {WAD_DIR, NULL};
|
||||
static int user = SCE_USER_SYSTEM;
|
||||
static int log_file = -1;
|
||||
static int test_cpu_present;
|
||||
|
||||
int ps5_user(void)
|
||||
{
|
||||
return user;
|
||||
}
|
||||
|
||||
static void log_text(const char *text, size_t length)
|
||||
{
|
||||
char line[512];
|
||||
size_t start = 0, i;
|
||||
|
||||
if (log_file >= 0)
|
||||
write(log_file, text, length);
|
||||
for (i = 0; i < length; i++)
|
||||
{
|
||||
if (text[i] != '\n' && i + 1 < length && i - start < sizeof(line) - 2)
|
||||
continue;
|
||||
memcpy(line, text + start, i - start + 1);
|
||||
line[i - start + 1] = 0;
|
||||
sceKernelDebugOutText(0, line);
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
static void *pump_output(void *arg)
|
||||
{
|
||||
int pipe_read = (int)(intptr_t)arg;
|
||||
char chunk[1024];
|
||||
ssize_t length;
|
||||
|
||||
while ((length = read(pipe_read, chunk, sizeof(chunk) - 1)) > 0)
|
||||
{
|
||||
chunk[length] = 0;
|
||||
log_text(chunk, (size_t)length);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void capture_output(void)
|
||||
{
|
||||
pthread_t thread;
|
||||
FILE *output;
|
||||
int fds[2];
|
||||
|
||||
log_file = open(LOG_PATH, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
if (log_file < 0)
|
||||
plat_log("ps5: cannot write %s (errno %d)\n", LOG_PATH, errno);
|
||||
if (pipe(fds))
|
||||
{
|
||||
plat_log("ps5: no output pipe (errno %d)\n", errno);
|
||||
return;
|
||||
}
|
||||
output = fdopen(fds[1], "w");
|
||||
if (!output)
|
||||
{
|
||||
plat_log("ps5: cannot redirect output (errno %d)\n", errno);
|
||||
return;
|
||||
}
|
||||
setvbuf(output, NULL, _IOLBF, 0);
|
||||
stdout = output;
|
||||
stderr = output;
|
||||
pthread_create(&thread, NULL, pump_output, (void *)(intptr_t)fds[0]);
|
||||
}
|
||||
|
||||
int ps5_test_cpu_present(void)
|
||||
{
|
||||
return test_cpu_present;
|
||||
}
|
||||
|
||||
static void load_test_plan(void)
|
||||
{
|
||||
static char text[TEST_PLAN_BYTES];
|
||||
char *line, *save;
|
||||
ssize_t length;
|
||||
int fd = open(TEST_PLAN_PATH, O_RDONLY);
|
||||
|
||||
if (fd < 0)
|
||||
return;
|
||||
length = read(fd, text, sizeof(text) - 1);
|
||||
close(fd);
|
||||
if (length <= 0)
|
||||
return;
|
||||
text[length] = 0;
|
||||
|
||||
for (line = strtok_r(text, "\r\n", &save); line; line = strtok_r(NULL, "\r\n", &save))
|
||||
{
|
||||
if (!strncmp(line, "input ", 6))
|
||||
test_plan_input(line + 6);
|
||||
else if (!strncmp(line, "capture ", 8))
|
||||
test_plan_captures(line + 8);
|
||||
else if (!strncmp(line, "frames ", 7))
|
||||
test_plan_limit(atoi(line + 7));
|
||||
else if (!strncmp(line, "serve ", 6))
|
||||
ps5_capture_serve(atoi(line + 6));
|
||||
else if (!strcmp(line, "present cpu"))
|
||||
test_cpu_present = 1;
|
||||
}
|
||||
plat_log("ps5: test plan loaded\n");
|
||||
}
|
||||
|
||||
int plat_init(int argc, char **argv)
|
||||
{
|
||||
int initial;
|
||||
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
capture_output();
|
||||
|
||||
if (sceUserServiceInitialize(NULL) < 0)
|
||||
plat_log("ps5: user service unavailable\n");
|
||||
else if (sceUserServiceGetInitialUser(&initial) == 0)
|
||||
user = initial;
|
||||
plat_log("ps5: user %d\n", user);
|
||||
load_test_plan();
|
||||
|
||||
ps5_pad_open();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void plat_shutdown(void)
|
||||
{
|
||||
ps5_pad_close();
|
||||
}
|
||||
|
||||
_Noreturn void plat_exit(int code)
|
||||
{
|
||||
plat_log("ps5: exit %d\n", code);
|
||||
fflush(NULL);
|
||||
sceKernelUsleep(LOG_DRAIN_US);
|
||||
sceSystemServiceLoadExec("exit", NULL);
|
||||
for (;;)
|
||||
sceKernelUsleep(1000000);
|
||||
}
|
||||
|
||||
void plat_log(const char *fmt, ...)
|
||||
{
|
||||
char line[512];
|
||||
va_list args;
|
||||
int length;
|
||||
|
||||
va_start(args, fmt);
|
||||
length = vsnprintf(line, sizeof(line), fmt, args);
|
||||
va_end(args);
|
||||
if (length < 0)
|
||||
return;
|
||||
log_text(line, (size_t)length < sizeof(line) ? (size_t)length : sizeof(line) - 1);
|
||||
}
|
||||
|
||||
void plat_alert(const char *message)
|
||||
{
|
||||
SceNotificationRequest request;
|
||||
|
||||
memset(&request, 0, sizeof(request));
|
||||
snprintf(request.message, sizeof(request.message), "DOOM: %s", message);
|
||||
sceKernelSendNotificationRequest(0, &request, sizeof(request), 0);
|
||||
}
|
||||
|
||||
uint64_t plat_ticks_us(void)
|
||||
{
|
||||
return sceKernelGetProcessTime();
|
||||
}
|
||||
|
||||
void plat_sleep_us(uint32_t us)
|
||||
{
|
||||
sceKernelUsleep(us);
|
||||
}
|
||||
|
||||
const char *const *plat_wad_dirs(void)
|
||||
{
|
||||
return wad_dirs;
|
||||
}
|
||||
|
||||
const char *plat_save_dir(void)
|
||||
{
|
||||
return SAVE_DIR;
|
||||
}
|
||||
+204
-204
@@ -1,204 +1,204 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "gpu.h"
|
||||
#include "platform.h"
|
||||
#include "ps5.h"
|
||||
#include "sce.h"
|
||||
#include "test_plan.h"
|
||||
#include "tiling.h"
|
||||
|
||||
#define OUT_WIDTH 1920
|
||||
#define OUT_HEIGHT 1080
|
||||
#define VIEW_WIDTH 1440
|
||||
#define VIEW_X ((OUT_WIDTH - VIEW_WIDTH) / 2)
|
||||
#define BUFFER_BYTES 0x1000000
|
||||
#define FRAME_PIXELS tiled_pixels(OUT_WIDTH, OUT_HEIGHT)
|
||||
#define FRAME_BYTES ((size_t)FRAME_PIXELS * 4)
|
||||
#define FORCE_CPU_BUTTONS (PAD_L1 | PAD_R1)
|
||||
#define CAPTURE_DRAIN_US 20000000
|
||||
|
||||
static int video = -1;
|
||||
static uint32_t *buffers[GPU_TARGETS];
|
||||
static int next_buffer;
|
||||
static int64_t flips;
|
||||
static int use_gpu;
|
||||
static int splash_hidden;
|
||||
|
||||
static int src_width, src_height;
|
||||
static uint16_t column_of[VIEW_WIDTH];
|
||||
static uint16_t row_of[OUT_HEIGHT];
|
||||
static uint32_t tile_columns[VIEW_WIDTH];
|
||||
static uint32_t rgba[256];
|
||||
|
||||
static int open_video(void)
|
||||
{
|
||||
SceVideoOutBuffer out[GPU_TARGETS];
|
||||
SceVideoOutAttribute attribute = {0};
|
||||
uint8_t *memory;
|
||||
int i, result;
|
||||
|
||||
video = sceVideoOutOpen(SCE_USER_SYSTEM, SCE_VIDEO_BUS_MAIN, 0, NULL);
|
||||
if (video < 0)
|
||||
{
|
||||
plat_log("video: open %#x\n", video);
|
||||
return -1;
|
||||
}
|
||||
memory = ps5_gpu_memory(BUFFER_BYTES * GPU_TARGETS);
|
||||
if (!memory)
|
||||
return -1;
|
||||
|
||||
memset(out, 0, sizeof(out));
|
||||
for (i = 0; i < GPU_TARGETS; i++)
|
||||
{
|
||||
uint32_t *p;
|
||||
|
||||
buffers[i] = (uint32_t *)(memory + i * BUFFER_BYTES);
|
||||
for (p = buffers[i]; p < buffers[i] + FRAME_PIXELS; p++)
|
||||
*p = 0xff000000u;
|
||||
ps5_cache_flush(buffers[i], FRAME_BYTES);
|
||||
out[i].data = buffers[i];
|
||||
}
|
||||
|
||||
sceVideoOutSetFlipRate(video, 0);
|
||||
sceVideoOutSetBufferAttribute2(&attribute, SCE_VIDEO_FORMAT_RGBA8_SRGB, SCE_VIDEO_TILING_TILED,
|
||||
OUT_WIDTH, OUT_HEIGHT, 0, 0, 0);
|
||||
result = sceVideoOutRegisterBuffers2(video, 0, 0, out, GPU_TARGETS, &attribute, 0, NULL);
|
||||
if (result < 0)
|
||||
{
|
||||
plat_log("video: register buffers %#x\n", result);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int gpu_wanted(void)
|
||||
{
|
||||
pad_state_t pad;
|
||||
|
||||
if (plat_pad_read(&pad) == 0 && (pad.buttons & FORCE_CPU_BUTTONS) == FORCE_CPU_BUTTONS)
|
||||
{
|
||||
plat_log("video: L1+R1 held, using the CPU scaler\n");
|
||||
return 0;
|
||||
}
|
||||
return !ps5_test_cpu_present();
|
||||
}
|
||||
|
||||
int ps5_frame(void)
|
||||
{
|
||||
return (int)flips;
|
||||
}
|
||||
|
||||
static void capture(int buffer)
|
||||
{
|
||||
char name[32];
|
||||
|
||||
if (use_gpu && gpu_finish())
|
||||
return;
|
||||
snprintf(name, sizeof(name), "shot%05d", (int)flips);
|
||||
ps5_cache_flush(buffers[buffer], FRAME_BYTES);
|
||||
ps5_capture_add(name, buffers[buffer], FRAME_BYTES);
|
||||
plat_log("video: captured frame %d\n", (int)flips);
|
||||
}
|
||||
|
||||
int plat_video_init(int width, int height)
|
||||
{
|
||||
gpu_config_t config;
|
||||
int i;
|
||||
|
||||
src_width = width;
|
||||
src_height = height;
|
||||
for (i = 0; i < VIEW_WIDTH; i++)
|
||||
{
|
||||
column_of[i] = (uint16_t)(i * width / VIEW_WIDTH);
|
||||
tile_columns[i] = tile_column(VIEW_X + i);
|
||||
}
|
||||
for (i = 0; i < OUT_HEIGHT; i++)
|
||||
row_of[i] = (uint16_t)(i * height / OUT_HEIGHT);
|
||||
|
||||
if (open_video())
|
||||
return -1;
|
||||
|
||||
config.video = video;
|
||||
for (i = 0; i < GPU_TARGETS; i++)
|
||||
config.targets[i] = buffers[i];
|
||||
config.width = OUT_WIDTH;
|
||||
config.height = OUT_HEIGHT;
|
||||
config.pitch = OUT_WIDTH;
|
||||
config.view_x = VIEW_X;
|
||||
config.view_width = VIEW_WIDTH;
|
||||
config.src_width = width;
|
||||
config.src_height = height;
|
||||
use_gpu = gpu_wanted() && gpu_init(&config) == 0;
|
||||
plat_log("video: %s presentation\n", use_gpu ? "AGC compute" : "CPU");
|
||||
return 0;
|
||||
}
|
||||
|
||||
void plat_video_shutdown(void)
|
||||
{
|
||||
if (video < 0)
|
||||
return;
|
||||
sceVideoOutClose(video);
|
||||
video = -1;
|
||||
}
|
||||
|
||||
static void cpu_scale(const uint8_t *pixels, uint32_t *target)
|
||||
{
|
||||
int x, y;
|
||||
|
||||
for (y = 0; y < OUT_HEIGHT; y++)
|
||||
{
|
||||
const uint8_t *src = pixels + row_of[y] * src_width;
|
||||
uint32_t *band = target + tile_row(y, OUT_WIDTH);
|
||||
unsigned int row = tile_offset(0, y % TILE_HEIGHT);
|
||||
|
||||
for (x = 0; x < VIEW_WIDTH; x++)
|
||||
band[tile_columns[x] ^ row] = rgba[src[column_of[x]]];
|
||||
}
|
||||
ps5_cache_flush(target, FRAME_BYTES);
|
||||
}
|
||||
|
||||
static void cpu_present(const uint8_t *pixels)
|
||||
{
|
||||
while (sceVideoOutIsFlipPending(video) > 0)
|
||||
sceKernelUsleep(500);
|
||||
cpu_scale(pixels, buffers[next_buffer]);
|
||||
sceVideoOutSubmitFlip(video, next_buffer, SCE_VIDEO_FLIP_VSYNC, flips);
|
||||
}
|
||||
|
||||
void plat_video_present(const uint8_t *pixels, const uint32_t *palette)
|
||||
{
|
||||
int i;
|
||||
|
||||
if (video < 0)
|
||||
return;
|
||||
for (i = 0; i < 256; i++)
|
||||
{
|
||||
uint32_t c = palette[i];
|
||||
|
||||
rgba[i] = (c & 0xff00ff00u) | (c >> 16 & 0xff) | (c & 0xff) << 16;
|
||||
}
|
||||
|
||||
flips++;
|
||||
if (use_gpu && gpu_present(pixels, rgba, next_buffer, flips))
|
||||
{
|
||||
plat_log("video: GPU presentation failed, switching to the CPU scaler\n");
|
||||
use_gpu = 0;
|
||||
}
|
||||
if (!use_gpu)
|
||||
cpu_present(pixels);
|
||||
if (test_plan_captures_frame((int)flips))
|
||||
capture(next_buffer);
|
||||
next_buffer = (next_buffer + 1) % GPU_TARGETS;
|
||||
if (test_plan_finished((int)flips))
|
||||
{
|
||||
ps5_capture_drain(CAPTURE_DRAIN_US);
|
||||
plat_exit(0);
|
||||
}
|
||||
|
||||
if (!splash_hidden)
|
||||
{
|
||||
sceSystemServiceHideSplashScreen();
|
||||
splash_hidden = 1;
|
||||
}
|
||||
}
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "gpu.h"
|
||||
#include "platform.h"
|
||||
#include "ps5.h"
|
||||
#include "sce.h"
|
||||
#include "test_plan.h"
|
||||
#include "tiling.h"
|
||||
|
||||
#define OUT_WIDTH 1920
|
||||
#define OUT_HEIGHT 1080
|
||||
#define VIEW_WIDTH 1440
|
||||
#define VIEW_X ((OUT_WIDTH - VIEW_WIDTH) / 2)
|
||||
#define BUFFER_BYTES 0x1000000
|
||||
#define FRAME_PIXELS tiled_pixels(OUT_WIDTH, OUT_HEIGHT)
|
||||
#define FRAME_BYTES ((size_t)FRAME_PIXELS * 4)
|
||||
#define FORCE_CPU_BUTTONS (PAD_L1 | PAD_R1)
|
||||
#define CAPTURE_DRAIN_US 20000000
|
||||
|
||||
static int video = -1;
|
||||
static uint32_t *buffers[GPU_TARGETS];
|
||||
static int next_buffer;
|
||||
static int64_t flips;
|
||||
static int use_gpu;
|
||||
static int splash_hidden;
|
||||
|
||||
static int src_width, src_height;
|
||||
static uint16_t column_of[VIEW_WIDTH];
|
||||
static uint16_t row_of[OUT_HEIGHT];
|
||||
static uint32_t tile_columns[VIEW_WIDTH];
|
||||
static uint32_t rgba[256];
|
||||
|
||||
static int open_video(void)
|
||||
{
|
||||
SceVideoOutBuffer out[GPU_TARGETS];
|
||||
SceVideoOutAttribute attribute = {0};
|
||||
uint8_t *memory;
|
||||
int i, result;
|
||||
|
||||
video = sceVideoOutOpen(SCE_USER_SYSTEM, SCE_VIDEO_BUS_MAIN, 0, NULL);
|
||||
if (video < 0)
|
||||
{
|
||||
plat_log("video: open %#x\n", video);
|
||||
return -1;
|
||||
}
|
||||
memory = ps5_gpu_memory(BUFFER_BYTES * GPU_TARGETS);
|
||||
if (!memory)
|
||||
return -1;
|
||||
|
||||
memset(out, 0, sizeof(out));
|
||||
for (i = 0; i < GPU_TARGETS; i++)
|
||||
{
|
||||
uint32_t *p;
|
||||
|
||||
buffers[i] = (uint32_t *)(memory + i * BUFFER_BYTES);
|
||||
for (p = buffers[i]; p < buffers[i] + FRAME_PIXELS; p++)
|
||||
*p = 0xff000000u;
|
||||
ps5_cache_flush(buffers[i], FRAME_BYTES);
|
||||
out[i].data = buffers[i];
|
||||
}
|
||||
|
||||
sceVideoOutSetFlipRate(video, 0);
|
||||
sceVideoOutSetBufferAttribute2(&attribute, SCE_VIDEO_FORMAT_RGBA8_SRGB, SCE_VIDEO_TILING_TILED,
|
||||
OUT_WIDTH, OUT_HEIGHT, 0, 0, 0);
|
||||
result = sceVideoOutRegisterBuffers2(video, 0, 0, out, GPU_TARGETS, &attribute, 0, NULL);
|
||||
if (result < 0)
|
||||
{
|
||||
plat_log("video: register buffers %#x\n", result);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int gpu_wanted(void)
|
||||
{
|
||||
pad_state_t pad;
|
||||
|
||||
if (plat_pad_read(&pad) == 0 && (pad.buttons & FORCE_CPU_BUTTONS) == FORCE_CPU_BUTTONS)
|
||||
{
|
||||
plat_log("video: L1+R1 held, using the CPU scaler\n");
|
||||
return 0;
|
||||
}
|
||||
return !ps5_test_cpu_present();
|
||||
}
|
||||
|
||||
int ps5_frame(void)
|
||||
{
|
||||
return (int)flips;
|
||||
}
|
||||
|
||||
static void capture(int buffer)
|
||||
{
|
||||
char name[32];
|
||||
|
||||
if (use_gpu && gpu_finish())
|
||||
return;
|
||||
snprintf(name, sizeof(name), "shot%05d", (int)flips);
|
||||
ps5_cache_flush(buffers[buffer], FRAME_BYTES);
|
||||
ps5_capture_add(name, buffers[buffer], FRAME_BYTES);
|
||||
plat_log("video: captured frame %d\n", (int)flips);
|
||||
}
|
||||
|
||||
int plat_video_init(int width, int height)
|
||||
{
|
||||
gpu_config_t config;
|
||||
int i;
|
||||
|
||||
src_width = width;
|
||||
src_height = height;
|
||||
for (i = 0; i < VIEW_WIDTH; i++)
|
||||
{
|
||||
column_of[i] = (uint16_t)(i * width / VIEW_WIDTH);
|
||||
tile_columns[i] = tile_column(VIEW_X + i);
|
||||
}
|
||||
for (i = 0; i < OUT_HEIGHT; i++)
|
||||
row_of[i] = (uint16_t)(i * height / OUT_HEIGHT);
|
||||
|
||||
if (open_video())
|
||||
return -1;
|
||||
|
||||
config.video = video;
|
||||
for (i = 0; i < GPU_TARGETS; i++)
|
||||
config.targets[i] = buffers[i];
|
||||
config.width = OUT_WIDTH;
|
||||
config.height = OUT_HEIGHT;
|
||||
config.pitch = OUT_WIDTH;
|
||||
config.view_x = VIEW_X;
|
||||
config.view_width = VIEW_WIDTH;
|
||||
config.src_width = width;
|
||||
config.src_height = height;
|
||||
use_gpu = gpu_wanted() && gpu_init(&config) == 0;
|
||||
plat_log("video: %s presentation\n", use_gpu ? "AGC compute" : "CPU");
|
||||
return 0;
|
||||
}
|
||||
|
||||
void plat_video_shutdown(void)
|
||||
{
|
||||
if (video < 0)
|
||||
return;
|
||||
sceVideoOutClose(video);
|
||||
video = -1;
|
||||
}
|
||||
|
||||
static void cpu_scale(const uint8_t *pixels, uint32_t *target)
|
||||
{
|
||||
int x, y;
|
||||
|
||||
for (y = 0; y < OUT_HEIGHT; y++)
|
||||
{
|
||||
const uint8_t *src = pixels + row_of[y] * src_width;
|
||||
uint32_t *band = target + tile_row(y, OUT_WIDTH);
|
||||
unsigned int row = tile_offset(0, y % TILE_HEIGHT);
|
||||
|
||||
for (x = 0; x < VIEW_WIDTH; x++)
|
||||
band[tile_columns[x] ^ row] = rgba[src[column_of[x]]];
|
||||
}
|
||||
ps5_cache_flush(target, FRAME_BYTES);
|
||||
}
|
||||
|
||||
static void cpu_present(const uint8_t *pixels)
|
||||
{
|
||||
while (sceVideoOutIsFlipPending(video) > 0)
|
||||
sceKernelUsleep(500);
|
||||
cpu_scale(pixels, buffers[next_buffer]);
|
||||
sceVideoOutSubmitFlip(video, next_buffer, SCE_VIDEO_FLIP_VSYNC, flips);
|
||||
}
|
||||
|
||||
void plat_video_present(const uint8_t *pixels, const uint32_t *palette)
|
||||
{
|
||||
int i;
|
||||
|
||||
if (video < 0)
|
||||
return;
|
||||
for (i = 0; i < 256; i++)
|
||||
{
|
||||
uint32_t c = palette[i];
|
||||
|
||||
rgba[i] = (c & 0xff00ff00u) | (c >> 16 & 0xff) | (c & 0xff) << 16;
|
||||
}
|
||||
|
||||
flips++;
|
||||
if (use_gpu && gpu_present(pixels, rgba, next_buffer, flips))
|
||||
{
|
||||
plat_log("video: GPU presentation failed, switching to the CPU scaler\n");
|
||||
use_gpu = 0;
|
||||
}
|
||||
if (!use_gpu)
|
||||
cpu_present(pixels);
|
||||
if (test_plan_captures_frame((int)flips))
|
||||
capture(next_buffer);
|
||||
next_buffer = (next_buffer + 1) % GPU_TARGETS;
|
||||
if (test_plan_finished((int)flips))
|
||||
{
|
||||
ps5_capture_drain(CAPTURE_DRAIN_US);
|
||||
plat_exit(0);
|
||||
}
|
||||
|
||||
if (!splash_hidden)
|
||||
{
|
||||
sceSystemServiceHideSplashScreen();
|
||||
splash_hidden = 1;
|
||||
}
|
||||
}
|
||||
+58
-58
@@ -1,58 +1,58 @@
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "png.h"
|
||||
|
||||
static unsigned group_x, item_x;
|
||||
|
||||
#define __kernel
|
||||
#define __global
|
||||
#define __attribute__(x)
|
||||
#define __builtin_amdgcn_workgroup_id_x() group_x
|
||||
#define __builtin_amdgcn_workitem_id_x() item_x
|
||||
#include "present.cl"
|
||||
|
||||
#define SRC_W 320
|
||||
#define SRC_H 200
|
||||
#define OUT_W 1920
|
||||
#define OUT_H 1080
|
||||
#define VIEW_W 1440
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
static uint8_t pixels[SRC_W * SRC_H];
|
||||
static uint32_t palette[256], rgba[256];
|
||||
uint32_t *out = calloc(tiled_pixels(OUT_W, OUT_H), 4);
|
||||
uint8_t *rgb = malloc(OUT_W * OUT_H * 3);
|
||||
FILE *f;
|
||||
int i;
|
||||
|
||||
if (argc != 3 || !(f = fopen(argv[1], "rb")))
|
||||
{
|
||||
fprintf(stderr, "usage: present_preview <frame.raw> <out.png>\n");
|
||||
return 2;
|
||||
}
|
||||
if (fread(pixels, 1, sizeof(pixels), f) != sizeof(pixels) ||
|
||||
fread(palette, 4, 256, f) != 256)
|
||||
return 1;
|
||||
fclose(f);
|
||||
|
||||
for (i = 0; i < 256; i++)
|
||||
rgba[i] = (palette[i] & 0xff00ff00u) | (palette[i] >> 16 & 0xff) | (palette[i] & 0xff) << 16;
|
||||
|
||||
for (group_x = 0; group_x < OUT_H; group_x++)
|
||||
for (item_x = 0; item_x < GROUP; item_x++)
|
||||
present(pixels, rgba, out, OUT_W, OUT_W, OUT_H, (OUT_W - VIEW_W) / 2, VIEW_W,
|
||||
SRC_W, SRC_H);
|
||||
|
||||
for (i = 0; i < OUT_W * OUT_H; i++)
|
||||
{
|
||||
uint32_t c = out[tiled_index(i % OUT_W, i / OUT_W, OUT_W)];
|
||||
|
||||
rgb[i * 3] = c & 0xff;
|
||||
rgb[i * 3 + 1] = c >> 8 & 0xff;
|
||||
rgb[i * 3 + 2] = c >> 16 & 0xff;
|
||||
}
|
||||
return png_write_rgb(argv[2], rgb, OUT_W, OUT_H);
|
||||
}
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "png.h"
|
||||
|
||||
static unsigned group_x, item_x;
|
||||
|
||||
#define __kernel
|
||||
#define __global
|
||||
#define __attribute__(x)
|
||||
#define __builtin_amdgcn_workgroup_id_x() group_x
|
||||
#define __builtin_amdgcn_workitem_id_x() item_x
|
||||
#include "present.cl"
|
||||
|
||||
#define SRC_W 320
|
||||
#define SRC_H 200
|
||||
#define OUT_W 1920
|
||||
#define OUT_H 1080
|
||||
#define VIEW_W 1440
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
static uint8_t pixels[SRC_W * SRC_H];
|
||||
static uint32_t palette[256], rgba[256];
|
||||
uint32_t *out = calloc(tiled_pixels(OUT_W, OUT_H), 4);
|
||||
uint8_t *rgb = malloc(OUT_W * OUT_H * 3);
|
||||
FILE *f;
|
||||
int i;
|
||||
|
||||
if (argc != 3 || !(f = fopen(argv[1], "rb")))
|
||||
{
|
||||
fprintf(stderr, "usage: present_preview <frame.raw> <out.png>\n");
|
||||
return 2;
|
||||
}
|
||||
if (fread(pixels, 1, sizeof(pixels), f) != sizeof(pixels) ||
|
||||
fread(palette, 4, 256, f) != 256)
|
||||
return 1;
|
||||
fclose(f);
|
||||
|
||||
for (i = 0; i < 256; i++)
|
||||
rgba[i] = (palette[i] & 0xff00ff00u) | (palette[i] >> 16 & 0xff) | (palette[i] & 0xff) << 16;
|
||||
|
||||
for (group_x = 0; group_x < OUT_H; group_x++)
|
||||
for (item_x = 0; item_x < GROUP; item_x++)
|
||||
present(pixels, rgba, out, OUT_W, OUT_W, OUT_H, (OUT_W - VIEW_W) / 2, VIEW_W,
|
||||
SRC_W, SRC_H);
|
||||
|
||||
for (i = 0; i < OUT_W * OUT_H; i++)
|
||||
{
|
||||
uint32_t c = out[tiled_index(i % OUT_W, i / OUT_W, OUT_W)];
|
||||
|
||||
rgb[i * 3] = c & 0xff;
|
||||
rgb[i * 3 + 1] = c >> 8 & 0xff;
|
||||
rgb[i * 3 + 2] = c >> 16 & 0xff;
|
||||
}
|
||||
return png_write_rgb(argv[2], rgb, OUT_W, OUT_H);
|
||||
}
|
||||
+160
-160
@@ -1,160 +1,160 @@
|
||||
"""Runs the deployed title on the PS5 with a test plan and collects what happened.
|
||||
|
||||
uv run --no-project --with pillow python tools/console_test.py test/console-play.cfg [--timeout S]
|
||||
|
||||
Writes the plan to the title folder as test.cfg (plus a capture port), launches PPSA99666 with
|
||||
the doomlaunch payload (tools/launcher, loaded through the Payload Manager), pulls the frames the
|
||||
plan captures from the running title over TCP, converts them to PNG under build/test/run/,
|
||||
prints the title's log lines from the kernel log (klogsrv capture in build/test/klog.txt) and
|
||||
removes test.cfg again. The PS5Upload helper must be running.
|
||||
"""
|
||||
import argparse
|
||||
import base64
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT.parent / "tools"))
|
||||
import ps5 # noqa: E402
|
||||
import push_ffpkg as push # noqa: E402
|
||||
|
||||
TITLE = "PPSA99666"
|
||||
PLDMGR = f"http://{ps5.PS5_IP}:8084"
|
||||
LAUNCHER = ROOT / "build" / "launcher" / "doomlaunch.elf"
|
||||
LAUNCHER_DIR = "/data/pldmgr/payloads/doomlaunch"
|
||||
TEST_PLAN = f"/data/homebrew/{TITLE}/test.cfg"
|
||||
CAPTURE_PORT = 9119
|
||||
KLOG = ROOT / "build" / "test" / "klog.txt"
|
||||
OUT = ROOT / "build" / "test" / "run"
|
||||
FRAME = (1920, 1080)
|
||||
NOISE = ("PFAuth", "FMEM", "PSM.UI", "catalog", "SHELLFLAG", "LOGIN MGR", "CloudMessaging",
|
||||
"ResArbitrator", "PlayGo", "AsyncStorage", "a53mm", "BAPM", "<118>", "[KERNEL]", "###",
|
||||
"num clients", "Amm config", "wt_application", "appmgr_create")
|
||||
|
||||
|
||||
def get(url, timeout=10):
|
||||
with urllib.request.urlopen(url, timeout=timeout) as r:
|
||||
return r.read().decode(errors="replace")
|
||||
|
||||
|
||||
def engine():
|
||||
push.LOG = ROOT / "build" / "deploy.log"
|
||||
push.start_engine()
|
||||
|
||||
|
||||
def write_remote(path, data):
|
||||
r = push.http("POST", "/api/ps5/fs/write-bytes",
|
||||
{"addr": push.XFER, "path": path, "bytes_b64": base64.b64encode(data).decode()})
|
||||
if "http_error" in r:
|
||||
sys.exit(f"writing {path} failed: {r}")
|
||||
|
||||
|
||||
def install_launcher():
|
||||
data = LAUNCHER.read_bytes()
|
||||
try:
|
||||
if any(e["name"] == LAUNCHER.name and e["size"] == len(data)
|
||||
for e in ps5.list_dir(LAUNCHER_DIR)["entries"]):
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
push.http("POST", "/api/ps5/fs/mkdir", {"addr": push.XFER, "path": LAUNCHER_DIR})
|
||||
write_remote(f"{LAUNCHER_DIR}/{LAUNCHER.name}", data)
|
||||
|
||||
|
||||
def receive_exactly(sock, size):
|
||||
data = bytearray()
|
||||
while len(data) < size:
|
||||
chunk = sock.recv(min(1 << 20, size - len(data)))
|
||||
if not chunk:
|
||||
raise ConnectionError("capture stream ended early")
|
||||
data += chunk
|
||||
return bytes(data)
|
||||
|
||||
|
||||
def tile_offset(x, y):
|
||||
bit = lambda v, n: (v >> n) & 1
|
||||
return (bit(x, 0) | bit(x, 1) << 1 | bit(y, 0) << 2 | bit(y, 1) << 3 | bit(y, 2) << 4 |
|
||||
bit(x, 2) << 5 | (bit(x, 3) ^ bit(y, 3)) << 6 | (bit(x, 4) ^ bit(y, 4)) << 7 |
|
||||
(bit(x, 6) ^ bit(y, 5)) << 8 | (bit(x, 5) ^ bit(y, 6)) << 9 | bit(y, 3) << 10 |
|
||||
bit(x, 4) << 11 | bit(y, 6) << 12 | bit(x, 6) << 13 | bit(x, 7) << 14 | bit(x, 8) << 15)
|
||||
|
||||
|
||||
def detile(data):
|
||||
import numpy as np
|
||||
|
||||
width, height = FRAME
|
||||
x = np.arange(width, dtype=np.int64)
|
||||
y = np.arange(height, dtype=np.int64)
|
||||
columns = (x // 512) * 65536 + tile_offset(x % 512, 0)
|
||||
rows = (y // 128) * 128 * width
|
||||
index = rows[:, None] + (columns[None, :] ^ tile_offset(0, y % 128)[:, None])
|
||||
pixels = np.frombuffer(data, dtype=np.uint32)[index]
|
||||
return pixels.astype("<u4").tobytes()
|
||||
|
||||
|
||||
def pull_captures(seen):
|
||||
from PIL import Image
|
||||
|
||||
try:
|
||||
sock = socket.create_connection((ps5.PS5_IP, CAPTURE_PORT), timeout=2)
|
||||
except OSError:
|
||||
return
|
||||
sock.settimeout(30)
|
||||
try:
|
||||
while True:
|
||||
(name_length,) = struct.unpack("<I", receive_exactly(sock, 4))
|
||||
if not name_length:
|
||||
break
|
||||
name = receive_exactly(sock, name_length).decode()
|
||||
(size,) = struct.unpack("<Q", receive_exactly(sock, 8))
|
||||
data = receive_exactly(sock, size)
|
||||
Image.frombuffer("RGBA", FRAME, detile(data), "raw", "RGBA", 0, 1).convert("RGB").save(OUT / f"{name}.png")
|
||||
seen.add(name)
|
||||
print(f"captured {name}", flush=True)
|
||||
except (OSError, ConnectionError) as e:
|
||||
print(f"capture pull interrupted: {e}", flush=True)
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("plan")
|
||||
ap.add_argument("--timeout", type=int, default=120)
|
||||
args = ap.parse_args()
|
||||
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
for old in OUT.glob("*"):
|
||||
old.unlink()
|
||||
engine()
|
||||
install_launcher()
|
||||
plan = Path(args.plan).read_text().rstrip("\n") + f"\nserve {CAPTURE_PORT}\n"
|
||||
write_remote(TEST_PLAN, plan.encode())
|
||||
|
||||
start = KLOG.stat().st_size
|
||||
print("launch:", get(f"{PLDMGR}/loadpayload:{LAUNCHER.name}", timeout=30).strip()[:200], flush=True)
|
||||
seen = set()
|
||||
deadline = time.time() + args.timeout
|
||||
exited = False
|
||||
try:
|
||||
while time.time() < deadline and not exited:
|
||||
pull_captures(seen)
|
||||
text = KLOG.read_bytes()[start:].decode(errors="replace")
|
||||
exited = "ps5: exit" in text or "Terminating pid" in text
|
||||
time.sleep(1)
|
||||
finally:
|
||||
push.http("POST", "/api/ps5/fs/delete", {"addr": push.XFER, "path": TEST_PLAN})
|
||||
|
||||
text = KLOG.read_bytes()[start:].decode(errors="replace")
|
||||
lines = [l for l in text.splitlines() if l.strip() and not any(n in l for n in NOISE)]
|
||||
(OUT / "log.txt").write_text("\n".join(lines), encoding="utf-8")
|
||||
print("\n".join(lines[-80:]))
|
||||
print(f"\nexited={exited} captures={sorted(seen)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
"""Runs the deployed title on the PS5 with a test plan and collects what happened.
|
||||
|
||||
uv run --no-project --with pillow python tools/console_test.py test/console-play.cfg [--timeout S]
|
||||
|
||||
Writes the plan to the title folder as test.cfg (plus a capture port), launches PPSA99666 with
|
||||
the doomlaunch payload (tools/launcher, loaded through the Payload Manager), pulls the frames the
|
||||
plan captures from the running title over TCP, converts them to PNG under build/test/run/,
|
||||
prints the title's log lines from the kernel log (klogsrv capture in build/test/klog.txt) and
|
||||
removes test.cfg again. The PS5Upload helper must be running.
|
||||
"""
|
||||
import argparse
|
||||
import base64
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT.parent / "tools"))
|
||||
import ps5 # noqa: E402
|
||||
import push_ffpkg as push # noqa: E402
|
||||
|
||||
TITLE = "PPSA99666"
|
||||
PLDMGR = f"http://{ps5.PS5_IP}:8084"
|
||||
LAUNCHER = ROOT / "build" / "launcher" / "doomlaunch.elf"
|
||||
LAUNCHER_DIR = "/data/pldmgr/payloads/doomlaunch"
|
||||
TEST_PLAN = f"/data/homebrew/{TITLE}/test.cfg"
|
||||
CAPTURE_PORT = 9119
|
||||
KLOG = ROOT / "build" / "test" / "klog.txt"
|
||||
OUT = ROOT / "build" / "test" / "run"
|
||||
FRAME = (1920, 1080)
|
||||
NOISE = ("PFAuth", "FMEM", "PSM.UI", "catalog", "SHELLFLAG", "LOGIN MGR", "CloudMessaging",
|
||||
"ResArbitrator", "PlayGo", "AsyncStorage", "a53mm", "BAPM", "<118>", "[KERNEL]", "###",
|
||||
"num clients", "Amm config", "wt_application", "appmgr_create")
|
||||
|
||||
|
||||
def get(url, timeout=10):
|
||||
with urllib.request.urlopen(url, timeout=timeout) as r:
|
||||
return r.read().decode(errors="replace")
|
||||
|
||||
|
||||
def engine():
|
||||
push.LOG = ROOT / "build" / "deploy.log"
|
||||
push.start_engine()
|
||||
|
||||
|
||||
def write_remote(path, data):
|
||||
r = push.http("POST", "/api/ps5/fs/write-bytes",
|
||||
{"addr": push.XFER, "path": path, "bytes_b64": base64.b64encode(data).decode()})
|
||||
if "http_error" in r:
|
||||
sys.exit(f"writing {path} failed: {r}")
|
||||
|
||||
|
||||
def install_launcher():
|
||||
data = LAUNCHER.read_bytes()
|
||||
try:
|
||||
if any(e["name"] == LAUNCHER.name and e["size"] == len(data)
|
||||
for e in ps5.list_dir(LAUNCHER_DIR)["entries"]):
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
push.http("POST", "/api/ps5/fs/mkdir", {"addr": push.XFER, "path": LAUNCHER_DIR})
|
||||
write_remote(f"{LAUNCHER_DIR}/{LAUNCHER.name}", data)
|
||||
|
||||
|
||||
def receive_exactly(sock, size):
|
||||
data = bytearray()
|
||||
while len(data) < size:
|
||||
chunk = sock.recv(min(1 << 20, size - len(data)))
|
||||
if not chunk:
|
||||
raise ConnectionError("capture stream ended early")
|
||||
data += chunk
|
||||
return bytes(data)
|
||||
|
||||
|
||||
def tile_offset(x, y):
|
||||
bit = lambda v, n: (v >> n) & 1
|
||||
return (bit(x, 0) | bit(x, 1) << 1 | bit(y, 0) << 2 | bit(y, 1) << 3 | bit(y, 2) << 4 |
|
||||
bit(x, 2) << 5 | (bit(x, 3) ^ bit(y, 3)) << 6 | (bit(x, 4) ^ bit(y, 4)) << 7 |
|
||||
(bit(x, 6) ^ bit(y, 5)) << 8 | (bit(x, 5) ^ bit(y, 6)) << 9 | bit(y, 3) << 10 |
|
||||
bit(x, 4) << 11 | bit(y, 6) << 12 | bit(x, 6) << 13 | bit(x, 7) << 14 | bit(x, 8) << 15)
|
||||
|
||||
|
||||
def detile(data):
|
||||
import numpy as np
|
||||
|
||||
width, height = FRAME
|
||||
x = np.arange(width, dtype=np.int64)
|
||||
y = np.arange(height, dtype=np.int64)
|
||||
columns = (x // 512) * 65536 + tile_offset(x % 512, 0)
|
||||
rows = (y // 128) * 128 * width
|
||||
index = rows[:, None] + (columns[None, :] ^ tile_offset(0, y % 128)[:, None])
|
||||
pixels = np.frombuffer(data, dtype=np.uint32)[index]
|
||||
return pixels.astype("<u4").tobytes()
|
||||
|
||||
|
||||
def pull_captures(seen):
|
||||
from PIL import Image
|
||||
|
||||
try:
|
||||
sock = socket.create_connection((ps5.PS5_IP, CAPTURE_PORT), timeout=2)
|
||||
except OSError:
|
||||
return
|
||||
sock.settimeout(30)
|
||||
try:
|
||||
while True:
|
||||
(name_length,) = struct.unpack("<I", receive_exactly(sock, 4))
|
||||
if not name_length:
|
||||
break
|
||||
name = receive_exactly(sock, name_length).decode()
|
||||
(size,) = struct.unpack("<Q", receive_exactly(sock, 8))
|
||||
data = receive_exactly(sock, size)
|
||||
Image.frombuffer("RGBA", FRAME, detile(data), "raw", "RGBA", 0, 1).convert("RGB").save(OUT / f"{name}.png")
|
||||
seen.add(name)
|
||||
print(f"captured {name}", flush=True)
|
||||
except (OSError, ConnectionError) as e:
|
||||
print(f"capture pull interrupted: {e}", flush=True)
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("plan")
|
||||
ap.add_argument("--timeout", type=int, default=120)
|
||||
args = ap.parse_args()
|
||||
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
for old in OUT.glob("*"):
|
||||
old.unlink()
|
||||
engine()
|
||||
install_launcher()
|
||||
plan = Path(args.plan).read_text().rstrip("\n") + f"\nserve {CAPTURE_PORT}\n"
|
||||
write_remote(TEST_PLAN, plan.encode())
|
||||
|
||||
start = KLOG.stat().st_size
|
||||
print("launch:", get(f"{PLDMGR}/loadpayload:{LAUNCHER.name}", timeout=30).strip()[:200], flush=True)
|
||||
seen = set()
|
||||
deadline = time.time() + args.timeout
|
||||
exited = False
|
||||
try:
|
||||
while time.time() < deadline and not exited:
|
||||
pull_captures(seen)
|
||||
text = KLOG.read_bytes()[start:].decode(errors="replace")
|
||||
exited = "ps5: exit" in text or "Terminating pid" in text
|
||||
time.sleep(1)
|
||||
finally:
|
||||
push.http("POST", "/api/ps5/fs/delete", {"addr": push.XFER, "path": TEST_PLAN})
|
||||
|
||||
text = KLOG.read_bytes()[start:].decode(errors="replace")
|
||||
lines = [l for l in text.splitlines() if l.strip() and not any(n in l for n in NOISE)]
|
||||
(OUT / "log.txt").write_text("\n".join(lines), encoding="utf-8")
|
||||
print("\n".join(lines[-80:]))
|
||||
print(f"\nexited={exited} captures={sorted(seen)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user