Skip to content
This repository was archived by the owner on Jul 21, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
patches/fbdoom/000[1-7]-*.patch -whitespace
168 changes: 168 additions & 0 deletions Formula/fbdoom.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
require (Tap.fetch("automattic", "kandelo-homebrew").path/"Kandelo/formula_support/kandelo_formula_support").to_s

class Fbdoom < Formula
include KandeloFormulaSupport

desc "Framebuffer-native Doom engine for Kandelo"
homepage "https://github.com/maximevince/fbDOOM"
url "https://github.com/maximevince/fbDOOM/archive/17280163bc95e5d954d2efaa0633489b763b4cd1.tar.gz"
version "0.1.0"
sha256 "77f57cee68fed438dffdba96f6070b8975c16652a63ddf4fb967994e5585a38a"
license "GPL-2.0-or-later"

depends_on "binaryen" => :build
depends_on "wabt" => [:build, :test]
skip_clean "bin/fbdoom"

resource "chocolate-doom" do
url "https://github.com/chocolate-doom/chocolate-doom/archive/35fb1372d10756ca27eca05665bd8a7cebc71c05.tar.gz"
sha256 "dc62c13cab469e19e0ad295b2dd7e460263c637a39c51d3771e96dabb08ecab2"
end

resource "doom-shareware-test" do
url "https://distro.ibiblio.org/slitaz/sources/packages/d/doom1.wad"
sha256 "1d7d43be501e67d927e415e0b8f3e29c3bf33075e859721816f652a526cac771"
end

SOURCE_DATE_EPOCH = "1775830255".freeze

def install
kandelo_require_arch!("wasm32")
vendor_music_sources
apply_kandelo_patches

artifact = buildpath/"fbdoom/fbdoom"
kandelo_wasm_build do |root|
source_identity = "/usr/src/fbdoom-#{version}"
path_flags = [
"-ffile-prefix-map=#{buildpath}=#{source_identity}",
"-fdebug-prefix-map=#{buildpath}=#{source_identity}",
"-fmacro-prefix-map=#{buildpath}=#{source_identity}",
"-ffile-prefix-map=#{root}=/usr/src/kandelo",
"-fdebug-prefix-map=#{root}=/usr/src/kandelo",
"-fmacro-prefix-map=#{root}=/usr/src/kandelo",
]
cflags = [
"-O2",
"-DNORMALUNIX",
"-DLINUX",
"-D_DEFAULT_SOURCE",
"-Iopl",
*path_flags,
].join(" ")

ENV["SOURCE_DATE_EPOCH"] = SOURCE_DATE_EPOCH
system "make", "-C", "fbdoom", "clean"
system "make", "-C", "fbdoom", "-j#{ENV.make_jobs}",
"CC=#{kandelo_cc(root)}",
"LD=#{kandelo_cc(root)}",
"CFLAGS=#{cflags}",
"LDFLAGS=-Wl,--gc-sections",
"LIBS=-lm",
"NOSDL=1"

kandelo_validate_wasm_artifact(artifact, fork: :forbidden)
validate_imports!(artifact)
end

kandelo_install_bin(buildpath/"fbdoom", "fbdoom", "fbdoom")
end

def vendor_music_sources
resource("chocolate-doom").stage do
opl_dir = buildpath/"fbdoom/opl"
opl_dir.mkpath
%w[opl.c opl.h opl3.c opl3.h opl_internal.h opl_queue.c opl_queue.h].each do |name|
cp "opl/#{name}", opl_dir/name
end
%w[mus2mid.c mus2mid.h midifile.c midifile.h].each do |name|
cp "src/#{name}", buildpath/"fbdoom"/name
end
end
end

def apply_kandelo_patches
tap_root = Pathname(__dir__).parent
patches = (tap_root/"patches/fbdoom").glob("*.patch").sort
odie "fbDOOM patch set is incomplete" if patches.length != 8

script = <<~SH
set -euo pipefail
for patch in "$@"; do
git apply --check "$patch"
git apply "$patch"
done
SH
system kandelo_host_tool("bash"), "-c", script, "bash", *patches
end

def validate_imports!(artifact)
system "bash", "-c", <<~SH
set -euo pipefail
unexpected_env_imports=$(wasm-objdump -x #{artifact.to_s.shellescape} |
awk '/<- env[.]/ { sub(/^.*<- env[.]/, ""); print $1 }' |
grep -Ev '^(__channel_base|memory|setjmp|longjmp)$' || true)
if [ -n "$unexpected_env_imports" ]; then
echo "ERROR: fbDOOM contains unresolved non-ABI env imports" >&2
echo "$unexpected_env_imports" >&2
exit 1
fi
SH
end

test do
assert_equal "\0asm".b, File.binread(bin/"fbdoom", 4)

# The shareware data is staged only for this test and is never installed
# in the keg or bottle. It is the same immutable IWAD used by Kandelo's
# Doom browser demo.
resource("doom-shareware-test").stage testpath
wad = testpath/"doom1.wad"
assert_path_exists wad

# The timedemo loads and renders demo1 through Kandelo's Node host, then
# reaches fbDOOM's timing-report failure exit instead of its old NOSDL
# infinite loop. That path also covers the patched atexit callback
# signature instead of treating --version as game execution evidence.
node_output = kandelo_run_pty_wasm(
bin/"fbdoom", ["-iwad", "/doom1.wad", "-timedemo", "demo1"],
inputs: [],
env: {
"HOME" => "/tmp",
"KERNEL_CWD" => "/",
"TERM" => "xterm-256color",
"TIMEOUT" => "120000",
},
guest_files: { "/doom1.wad" => wad },
expected_status: 1
)
refute_includes node_output, "process timed out after"
assert_match(/timed \d+ gametics in \d+ realtics \([0-9.]+ fps\)/, node_output)

browser_output = kandelo_run_framebuffer_wasm(
bin/"fbdoom",
argv: ["-iwad", "/doom1.wad"],
guest_files: { "/doom1.wad" => wad },
min_writes: 2,
min_nonblank_pixels: 100_000,
timeout_ms: 45_000,
)
assert_match(
/^kandelo-framebuffer-ok
\s+binds=\d+
\s+writes=\d+
\s+bytes=\d+
\s+size=\d+x\d+
\s+format=\S+
\s+nonblank=\d+
\s+screenshot-bytes=\d+$/x,
browser_output,
)

binary = File.binread(bin/"fbdoom")
refute_includes binary, prefix.to_s
refute_includes binary, "/nix/store/"
refute_match %r{/private/tmp/[^/]+/}, binary
refute_match %r{/Users/[^/]+/}, binary
end
end
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ Current migration controls and pilots include:
Kandelo's musl `catopen` and `catgets` implementation.
- `ctags`, Universal Ctags' maintained tag generator, `readtags` query client,
and optscript interpreter with complete C and C++ workflows.
- `fbdoom`, the framebuffer-native Doom engine tested with a non-bundled shareware IWAD.

The SDK is not yet a Homebrew dependency. Trusted builds supply an
`HOMEBREW_KANDELO_ROOT` checkout containing the SDK, sysroot, kernel, and Node
Expand Down
13 changes: 13 additions & 0 deletions patches/fbdoom/0001-fix-I_InitInput-signature.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
diff --git a/fbdoom/i_video_fbdev.c b/fbdoom/i_video_fbdev.c
index c6e5925..a27b8ce 100644
--- a/fbdoom/i_video_fbdev.c
+++ b/fbdoom/i_video_fbdev.c
@@ -202,7 +202,7 @@ void I_InitGraphics (void)

screenvisible = true;

- extern int I_InitInput(void);
+ extern void I_InitInput(void);
I_InitInput();
}

105 changes: 105 additions & 0 deletions patches/fbdoom/0002-add-mice-input.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
diff --git a/fbdoom/i_input_tty.c b/fbdoom/i_input_tty.c
index 95e1b60..0fe7586 100644
--- a/fbdoom/i_input_tty.c
+++ b/fbdoom/i_input_tty.c
@@ -255,6 +255,28 @@ static int old_mode = -1;
static struct termios old_term;
static int kb = -1; /* keyboard file descriptor */

+/* /dev/input/mice file descriptor + last-known button bitmask. The host
+ canvas listeners push PS/2 packets into the kernel queue; we drain
+ them per-tick from I_GetEvent. */
+static int mice_fd = -1;
+static unsigned int mouse_buttons = 0;
+
+static void mice_init(void)
+{
+ mice_fd = open("/dev/input/mice", O_RDONLY | O_NONBLOCK);
+ if (mice_fd < 0) {
+ printf("Mouse: /dev/input/mice unavailable; mouse disabled.\n");
+ }
+}
+
+static void mice_shutdown(void)
+{
+ if (mice_fd >= 0) {
+ close(mice_fd);
+ mice_fd = -1;
+ }
+}
+
void kbd_shutdown(void)
{
/* Shut down nicely. */
@@ -271,6 +293,8 @@ void kbd_shutdown(void)
if (kb > 3)
close(kb);

+ mice_shutdown();
+
exit(0);
}

@@ -426,6 +450,43 @@ static void UpdateShiftStatus(int pressed, unsigned char key)
}


+/* Drain queued PS/2 packets from /dev/input/mice and post one coalesced
+ ev_mouse per input poll. DOOM stores mouse motion as a per-tic delta, so
+ posting one event per packet lets later packets in the same tic overwrite
+ earlier motion and makes fast pointer-lock movement feel muted.
+
+ PS/2 byte0 carries L/R/M button bits and sign/overflow flags; bytes 1..2
+ are signed dx/dy with positive-up semantics for dy. The host inverts
+ browser deltaY before injecting, so dy is already in PS/2 (and DOOM) sign
+ convention. */
+static void I_PollMice(void)
+{
+ unsigned char pkt[3];
+ event_t event;
+ int n;
+ int dx = 0;
+ int dy = 0;
+ unsigned int buttons = mouse_buttons;
+
+ if (mice_fd < 0) return;
+
+ while ((n = read(mice_fd, pkt, 3)) == 3)
+ {
+ buttons = pkt[0] & 0x07;
+ dx += (signed char)pkt[1];
+ dy += (signed char)pkt[2];
+ }
+
+ if (dx == 0 && dy == 0 && buttons == mouse_buttons) return;
+
+ mouse_buttons = buttons;
+ event.type = ev_mouse;
+ event.data1 = mouse_buttons;
+ event.data2 = dx;
+ event.data3 = dy;
+ D_PostEvent(&event);
+}
+
void I_GetEvent(void)
{
event_t event;
@@ -433,7 +494,9 @@ void I_GetEvent(void)
unsigned char key;

// put event-grabbing stuff in here
-
+
+ I_PollMice();
+
while (kbd_read(&pressed, &key))
{
if (key == 0x0E) {
@@ -494,6 +557,7 @@ void I_GetEvent(void)
void I_InitInput(void)
{
kbd_init();
+ mice_init();

//UpdateFocus();
}
Loading