commit 275afb190ec44547db433d466f8a84a7ff1cee19 from: Rohan Verma date: Sun Jul 12 15:25:21 2026 UTC swap to FLTK commit - b7bda67bcb33cd457e8311d404262aa47155d71d commit + 275afb190ec44547db433d466f8a84a7ff1cee19 blob - d5dd479a976603498bcca512cc386d34ae6f47df blob + 094bae55f5826a8c9b45e5bdcb654550169298f0 Binary files .DS_Store and .DS_Store differ blob - f5d5903678900c6e3877a59af8e9f3546bfe684d blob + deb55e8d233fa1c43d40c1ae0e750c382a8ea62c --- build.sh +++ build.sh @@ -1,8 +1,9 @@ #!/bin/sh set -e INC="-Ilib -Isrc" -CORE="src/ui.c src/audio.c src/aproc.c src/net.c src/update.c lib/microui.c" -CFLAGS="-Os -Wall -ffunction-sections -fdata-sections" +CORE="src/audio.c src/aproc.c src/net.c src/update.c" +CFLAGS="-Os -Wall" +GC="-ffunction-sections -fdata-sections" SIGNDIR="${SIGNDIR:-$HOME/.signify}" SIGNKEYNAME="${SIGNKEYNAME:-rverma-2026}" @@ -41,6 +42,7 @@ if [ -f "$SIGNPUB" ]; then fi fi CFLAGS="$CFLAGS $PUBDEF" +CXXFLAGS="$CFLAGS -std=c++11 -fno-exceptions -fno-rtti -D_THREAD_SAFE -D_REENTRANT" sign_artifact() { [ -f "$SIGNKEY" ] || return 0 @@ -73,6 +75,9 @@ if [ "$1" = "win" ]; then PFX=${WINPFX:-./deps-win-$WARCH} [ -d "$PFX" ] || PFX=/tmp/w64 [ -d "$PFX" ] || { echo "deps missing: run ./win-deps.sh $WARCH"; exit 1; } + [ -f "$PFX/lib/libfltk.a" ] || { echo "fltk missing: run ./win-deps.sh $WARCH"; exit 1; } + WCXX=${WINCXX:-$(echo "$WCC" | sed 's/clang$/clang++/; s/gcc-posix$/g++-posix/; s/gcc$/g++/')} + command -v "$WCXX" >/dev/null 2>&1 || { echo "no c++ compiler for $TRIPLE ($WCXX)"; exit 1; } ICON_OBJ="" if [ -f assets/milna.png ]; then @@ -107,13 +112,18 @@ if [ "$1" = "win" ]; then echo "icon: no assets/milna.png — exe has default icon" fi + UIOBJ=/tmp/milna_ui_win.o + $WCXX $CXXFLAGS $INC -I"$PFX/include" -D_WIN32_WINNT=0x0A00 \ + -c src/ui.cpp -o "$UIOBJ" $WCC $CFLAGS $INC -I"$PFX/include" -D_WIN32_WINNT=0x0A00 \ - src/plat_win.c src/vid_win.c src/cap_win.c src/cam_win.c $CORE \ + src/vid_win.c src/cap_win.c src/cam_win.c $CORE "$UIOBJ" \ $ICON_OBJ \ -o milna.exe \ + "$PFX/lib/libfltk.a" \ "$PFX/lib/libopus.a" "$PFX/lib/libspeexdsp.a" "$PFX/lib/libsodium.a" \ -lws2_32 -lmfplat -lmfreadwrite -lmf -lmfuuid -lwmcodecdspuuid -lstrmiids -lole32 -loleaut32 \ -lgdi32 -luser32 -lwinmm -lavrt \ + -lcomctl32 -lgdiplus -luuid -lwinspool -lstdc++ \ -static -mwindows -Wl,--gc-sections $TRIPLE-strip milna.exe 2>/dev/null || true echo "built ./milna.exe ($(du -h milna.exe | cut -f1))" @@ -128,15 +138,23 @@ Darwin) OPUS=$(brew --prefix opus 2>/dev/null || echo /opt/homebrew/opt/opus) SPX=$(brew --prefix speexdsp 2>/dev/null || echo /opt/homebrew/opt/speexdsp) SODIUM=$(brew --prefix libsodium 2>/dev/null || echo /opt/homebrew/opt/libsodium) - $CC $CFLAGS $INC -I"$OPUS/include" -I"$SPX/include" -I"$SODIUM/include" \ - src/plat_mac.m src/vid_mac.m src/cap_mac.m src/cam_mac.m $CORE -o $OUT \ + FLTK=$(brew --prefix fltk 2>/dev/null || echo /opt/homebrew/opt/fltk) + [ -f "$FLTK/lib/libfltk.a" ] || { echo "fltk missing: brew install fltk"; exit 1; } + CXX=${CXX:-c++} + UIOBJ=/tmp/milna_ui.o + $CXX $CXXFLAGS $INC -I"$FLTK/include" -c src/ui.cpp -o "$UIOBJ" + $CC $CFLAGS $GC $INC -I"$OPUS/include" -I"$SPX/include" -I"$SODIUM/include" \ + src/vid_mac.m src/cap_mac.m src/cam_mac.m $CORE "$UIOBJ" -o $OUT \ + "$FLTK/lib/libfltk.a" \ "$OPUS/lib/libopus.a" \ "$SPX/lib/libspeexdsp.a" \ "$SODIUM/lib/libsodium.a" \ + -lc++ \ -framework Cocoa -framework CoreGraphics \ -framework CoreAudio -framework AudioToolbox -framework AudioUnit \ -framework VideoToolbox -framework CoreMedia -framework CoreVideo \ -framework ScreenCaptureKit -framework AVFoundation \ + -weak_framework UniformTypeIdentifiers \ -Wl,-dead_strip strip "$OUT" 2>/dev/null || true echo "built ./$OUT ($(du -h $OUT | cut -f1))" @@ -204,10 +222,15 @@ PLIST exit 0 ;; Linux) - $CC $CFLAGS $INC $(pkg-config --cflags opus speexdsp vpx libsodium) \ - src/plat_x11.c src/vid.c src/cap_stub.c src/cam_stub.c $CORE -o $OUT \ + command -v fltk-config >/dev/null 2>&1 || { echo "fltk missing (need fltk-config)"; exit 1; } + CXX=${CXX:-g++} + UIOBJ=/tmp/milna_ui.o + $CXX $CXXFLAGS $INC $(fltk-config --cxxflags) -c src/ui.cpp -o "$UIOBJ" + $CC $CFLAGS $GC $INC $(pkg-config --cflags opus speexdsp vpx libsodium) \ + src/vid.c src/cap_stub.c src/cam_stub.c $CORE "$UIOBJ" -o $OUT \ $(pkg-config --libs opus speexdsp vpx libsodium) \ - -lX11 -lm -lpthread -ldl -Wl,--gc-sections + $(fltk-config --ldstaticflags) -lstdc++ \ + -lm -lpthread -ldl -Wl,--gc-sections ;; *) echo "unknown platform"; exit 1 blob - 8e9e5d3348746dacfa997357d4164ab152cbd10e (mode 644) blob + /dev/null --- lib/atlas.inl +++ /dev/null @@ -1,995 +0,0 @@ - -enum { ATLAS_WHITE = MU_ICON_MAX, ATLAS_FONT }; -enum { ATLAS_WIDTH = 128, ATLAS_HEIGHT = 128 }; - - -static unsigned char atlas_texture[ATLAS_WIDTH * ATLAS_HEIGHT] = { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xc0, 0x20, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x32, 0x0b, 0x00, 0x00, 0x00, - 0x00, 0x31, 0x35, 0x01, 0x00, 0x00, 0x00, 0x15, 0x35, 0x1d, 0x30, 0x19, - 0x00, 0x00, 0x0f, 0x35, 0x06, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x2d, 0x00, 0x00, 0x00, - 0x14, 0x26, 0x00, 0x00, 0x31, 0x2a, 0x00, 0x00, 0x00, 0x10, 0x31, 0x00, - 0x00, 0x00, 0x00, 0x06, 0x3e, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x06, 0x3e, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x40, 0x0d, 0x00, 0x00, 0x00, 0x00, - 0x31, 0x35, 0x2f, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x41, - 0x37, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x21, 0xe0, 0xea, 0x2c, 0x00, 0x00, 0x00, 0x00, - 0x36, 0xc4, 0xdb, 0xb2, 0xd9, 0xc1, 0x1a, 0x00, 0x00, 0xea, 0xff, 0x39, - 0x00, 0x00, 0x00, 0x9e, 0xff, 0x88, 0xbe, 0x9c, 0x00, 0x00, 0x72, 0xff, - 0x48, 0x00, 0x00, 0xbb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x1c, 0xe4, 0xce, 0x8d, 0x00, 0x00, 0xb5, 0x60, 0x00, 0x00, - 0xea, 0xfa, 0x2c, 0x00, 0x00, 0x4e, 0xeb, 0x00, 0x00, 0x1c, 0x8f, 0xea, - 0xea, 0xee, 0x92, 0x1f, 0x00, 0x00, 0x1c, 0x8f, 0xea, 0xea, 0xee, 0x92, - 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x42, 0xf3, 0xcd, 0xf5, 0x3a, 0x00, 0x00, 0x00, 0xea, 0xf2, 0xef, 0xe5, - 0x8f, 0x2f, 0x00, 0x00, 0x0f, 0xa0, 0xfe, 0xf2, 0xf1, 0xfa, 0x33, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, - 0xe0, 0xea, 0x2d, 0x00, 0x00, 0x00, 0x00, 0x35, 0xdc, 0x33, 0x00, 0x06, - 0x00, 0x5a, 0xd7, 0x13, 0x00, 0xea, 0xd8, 0x92, 0x00, 0x00, 0x09, 0xf0, - 0xd9, 0x88, 0x7b, 0xda, 0x00, 0x00, 0xb9, 0xe9, 0x91, 0x00, 0x05, 0xf4, - 0x03, 0x35, 0x02, 0x32, 0x1f, 0x00, 0x04, 0x37, 0x24, 0x00, 0x5b, 0xa9, - 0x1b, 0xe7, 0x01, 0x44, 0xd0, 0x02, 0x00, 0x00, 0xea, 0xe4, 0xc3, 0x01, - 0x00, 0x4e, 0xeb, 0x00, 0x00, 0x8f, 0xd8, 0x42, 0x01, 0x3a, 0xd0, 0x9b, - 0x00, 0x00, 0x8f, 0xd8, 0x42, 0x01, 0x3a, 0xd0, 0x9b, 0x00, 0x2e, 0x1a, - 0x00, 0x03, 0x36, 0x19, 0x00, 0x04, 0x36, 0x00, 0xa3, 0xa0, 0x00, 0xb5, - 0x8d, 0x00, 0x00, 0x00, 0xea, 0x6d, 0x01, 0x3d, 0xac, 0xe5, 0x03, 0x00, - 0xa1, 0xeb, 0x63, 0x0c, 0x03, 0x2e, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0xe1, 0xeb, 0x2d, 0x00, 0x00, - 0x00, 0x00, 0x00, 0xd6, 0x38, 0x37, 0xb4, 0xd6, 0xe9, 0x35, 0x9e, 0x5c, - 0x00, 0xea, 0x87, 0xe8, 0x03, 0x00, 0x56, 0xcc, 0xba, 0x88, 0x38, 0xff, - 0x1a, 0x09, 0xf7, 0x84, 0xd9, 0x00, 0x39, 0xfe, 0x0e, 0xff, 0xb7, 0xe1, - 0xf3, 0x94, 0xbc, 0xde, 0xfb, 0x97, 0x73, 0x9b, 0x0b, 0xfb, 0x0b, 0xcf, - 0x44, 0x01, 0x00, 0x00, 0xea, 0x65, 0xf7, 0x63, 0x00, 0x4e, 0xeb, 0x00, - 0x01, 0xe0, 0x79, 0x00, 0x00, 0x00, 0x6b, 0xea, 0x03, 0x01, 0xe0, 0x79, - 0x00, 0x00, 0x00, 0x6b, 0xea, 0x03, 0xb0, 0xa4, 0x00, 0x39, 0xfe, 0xa0, - 0x00, 0x3d, 0xfd, 0x00, 0x6e, 0xe0, 0x5b, 0xef, 0x41, 0x00, 0x00, 0x00, - 0xea, 0x6d, 0x00, 0x00, 0x17, 0xfd, 0x47, 0x18, 0xff, 0x8f, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x21, 0xe1, 0xeb, 0x2d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0xe3, - 0x00, 0xc5, 0x51, 0x00, 0xd4, 0x37, 0x61, 0x99, 0x00, 0xea, 0x4d, 0xe9, - 0x45, 0x00, 0xb2, 0x70, 0xbf, 0x88, 0x04, 0xf0, 0x59, 0x48, 0xe9, 0x18, - 0xfd, 0x23, 0x77, 0xd4, 0x0e, 0xff, 0x9f, 0x02, 0x54, 0xff, 0x86, 0x00, - 0x69, 0xf0, 0x39, 0xd1, 0x61, 0xca, 0x60, 0xb6, 0x6f, 0xe3, 0x98, 0x00, - 0xea, 0x4a, 0x7f, 0xec, 0x15, 0x4e, 0xeb, 0x00, 0x32, 0xff, 0x36, 0x00, - 0x00, 0x00, 0x29, 0xff, 0x3e, 0x32, 0xff, 0x36, 0x00, 0x00, 0x00, 0x29, - 0xff, 0x3f, 0x68, 0xe6, 0x00, 0x83, 0xaf, 0xe7, 0x01, 0x80, 0xc9, 0x00, - 0x18, 0xf4, 0xff, 0x53, 0x00, 0x15, 0x3b, 0x00, 0xea, 0x6d, 0x00, 0x00, - 0x00, 0xc5, 0xa2, 0x41, 0xff, 0x39, 0x00, 0x40, 0x73, 0x73, 0x35, 0x00, - 0x00, 0x00, 0x3d, 0xed, 0x45, 0x00, 0x00, 0x00, 0x22, 0xe1, 0xeb, 0x2d, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4f, 0xb3, 0x12, 0xf9, 0x04, 0x03, - 0xef, 0x2c, 0x6a, 0x94, 0x00, 0xea, 0x4d, 0x95, 0x9e, 0x14, 0xf6, 0x18, - 0xc1, 0x88, 0x00, 0xb2, 0x96, 0x8e, 0xa4, 0x00, 0xcb, 0x6a, 0xb5, 0x92, - 0x0e, 0xff, 0x59, 0x00, 0x27, 0xff, 0x3d, 0x00, 0x3a, 0xff, 0x04, 0x71, - 0xae, 0x40, 0xe0, 0x2e, 0xee, 0x1e, 0xd5, 0x00, 0xea, 0x4d, 0x08, 0xd9, - 0xa0, 0x4b, 0xeb, 0x00, 0x20, 0xfe, 0x45, 0x00, 0x00, 0x00, 0x37, 0xff, - 0x2c, 0x20, 0xfe, 0x45, 0x00, 0x00, 0x00, 0x37, 0xff, 0x3b, 0x21, 0xff, - 0x29, 0xcc, 0x4a, 0xe9, 0x30, 0xc3, 0x81, 0x14, 0xdf, 0xab, 0xbd, 0xcd, - 0x14, 0x9c, 0xb5, 0x00, 0xea, 0x6d, 0x00, 0x00, 0x00, 0xdc, 0xa6, 0x20, - 0xfe, 0x42, 0x00, 0x63, 0xb2, 0xf4, 0x76, 0x00, 0x00, 0x00, 0x13, 0xd0, - 0xf6, 0x45, 0x00, 0x22, 0xe1, 0xeb, 0x2e, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x3f, 0xe7, 0x03, 0xd7, 0x5e, 0x75, 0xf7, 0x8d, 0xc7, 0x4a, - 0x00, 0xea, 0x4d, 0x3c, 0xf0, 0x71, 0xb7, 0x00, 0xc1, 0x88, 0x00, 0x70, - 0xc9, 0xcb, 0x5d, 0x00, 0x83, 0xa2, 0xe9, 0x4f, 0x0e, 0xff, 0x43, 0x00, - 0x26, 0xff, 0x2d, 0x00, 0x39, 0xff, 0x00, 0x00, 0x00, 0x7c, 0x9a, 0x1f, - 0xf1, 0x00, 0xaf, 0x00, 0xea, 0x4d, 0x00, 0x44, 0xfe, 0x83, 0xeb, 0x00, - 0x00, 0xcd, 0x89, 0x00, 0x00, 0x00, 0x7b, 0xd9, 0x00, 0x00, 0xcd, 0x89, - 0x00, 0x00, 0x00, 0x7b, 0xf8, 0x08, 0x00, 0xd8, 0x75, 0xf8, 0x0d, 0xa7, - 0x79, 0xf7, 0x39, 0x5b, 0xfc, 0x0a, 0x09, 0xba, 0xd4, 0xf6, 0x39, 0x00, - 0xea, 0x6d, 0x00, 0x00, 0x31, 0xff, 0x75, 0x00, 0xcc, 0x8f, 0x00, 0x00, - 0x00, 0xdb, 0x76, 0x00, 0x00, 0x00, 0x00, 0x13, 0xd0, 0xf6, 0x63, 0xe1, - 0xeb, 0x2e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0xf3, - 0x44, 0x2c, 0x96, 0x87, 0x29, 0xa3, 0x64, 0x00, 0x00, 0xea, 0x4d, 0x01, - 0xe1, 0xf3, 0x5b, 0x00, 0xc1, 0x88, 0x00, 0x2e, 0xf7, 0xf5, 0x17, 0x00, - 0x3a, 0xec, 0xfc, 0x10, 0x0e, 0xff, 0x43, 0x00, 0x26, 0xff, 0x2d, 0x00, - 0x39, 0xff, 0x00, 0x00, 0x17, 0xe6, 0x19, 0x02, 0xee, 0x13, 0xd0, 0x00, - 0xea, 0x4d, 0x00, 0x00, 0xa5, 0xf6, 0xeb, 0x00, 0x00, 0x7d, 0xec, 0x7a, - 0x24, 0x73, 0xe7, 0x87, 0x00, 0x00, 0x7d, 0xec, 0x7a, 0x24, 0x73, 0xe7, - 0x9c, 0x00, 0x00, 0x91, 0xda, 0xbd, 0x00, 0x61, 0xd9, 0xed, 0x03, 0x2b, - 0xfe, 0x67, 0x1d, 0x70, 0xfc, 0xf0, 0x1a, 0x00, 0xea, 0x7f, 0x31, 0x81, - 0xdc, 0xdc, 0x0a, 0x00, 0x79, 0xef, 0x83, 0x23, 0x1c, 0xe1, 0x76, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x13, 0xd0, 0xff, 0xec, 0x2e, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6b, 0xdc, 0x78, 0x20, 0x0d, - 0x3c, 0x3b, 0x00, 0x00, 0x00, 0xea, 0x4d, 0x00, 0x8a, 0xf3, 0x0b, 0x00, - 0xc1, 0x88, 0x00, 0x01, 0xea, 0xce, 0x00, 0x00, 0x03, 0xed, 0xc9, 0x00, - 0x0e, 0xff, 0x43, 0x00, 0x26, 0xff, 0x2d, 0x00, 0x39, 0xff, 0x00, 0x00, - 0x98, 0x7e, 0x00, 0x00, 0x78, 0xe7, 0xb0, 0x00, 0xea, 0x4d, 0x00, 0x00, - 0x18, 0xef, 0xeb, 0x00, 0x00, 0x06, 0x54, 0xb6, 0xfb, 0xbb, 0x57, 0x07, - 0x00, 0x00, 0x06, 0x54, 0xb6, 0xfc, 0xff, 0x8d, 0x01, 0x00, 0x00, 0x49, - 0xff, 0x74, 0x00, 0x1b, 0xfe, 0xa8, 0x00, 0x00, 0x7f, 0xde, 0xff, 0xe7, - 0x72, 0xb1, 0xdb, 0x00, 0xea, 0xff, 0xf8, 0xd6, 0x92, 0x13, 0x00, 0x00, - 0x06, 0x52, 0xb4, 0xfb, 0xff, 0xe5, 0x55, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x13, 0xbc, 0x2e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x44, 0xb7, 0xde, 0xdb, 0xad, 0x50, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x1b, 0xda, 0xbb, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, - 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x26, 0xe6, - 0xa0, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0x17, 0x00, 0x00, 0x00, - 0x2a, 0x1e, 0x00, 0x35, 0x13, 0x00, 0x00, 0x00, 0x2c, 0x1c, 0x00, 0x00, - 0x0c, 0x27, 0x00, 0x29, 0x09, 0x00, 0x00, 0x00, 0x3c, 0x24, 0x00, 0x00, - 0x00, 0x31, 0x35, 0x30, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x44, - 0x1f, 0x00, 0x00, 0x31, 0x17, 0x00, 0x00, 0x13, 0x35, 0x00, 0x31, 0x35, - 0x2b, 0x02, 0x00, 0x00, 0x00, 0x31, 0x35, 0x2a, 0x02, 0x00, 0x00, 0x33, - 0x16, 0x00, 0x00, 0x00, 0x0f, 0x35, 0x27, 0x28, 0x00, 0x00, 0x00, 0x1f, - 0x2e, 0x0b, 0xbc, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x13, 0xbc, 0x2a, 0x0b, 0xbc, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0xea, 0x6d, 0x00, 0x00, 0x00, 0xc9, 0x8e, 0x00, 0xfc, - 0x5b, 0x00, 0x00, 0x00, 0xd2, 0x84, 0x00, 0x00, 0x59, 0xa9, 0x00, 0xea, - 0x0f, 0x00, 0x00, 0x34, 0xfe, 0xce, 0x00, 0x00, 0x00, 0xea, 0xf2, 0xed, - 0xfa, 0xce, 0x1a, 0x00, 0x09, 0xb5, 0xfd, 0xe7, 0xf5, 0xaf, 0x00, 0xea, - 0x6d, 0x00, 0x0c, 0xcf, 0xa7, 0x00, 0xea, 0xf2, 0xf3, 0xea, 0x94, 0x00, - 0x00, 0xea, 0xf4, 0xf6, 0xec, 0x9d, 0x00, 0xc0, 0x9e, 0x00, 0x00, 0x00, - 0x7a, 0xe5, 0x58, 0xf8, 0x26, 0x00, 0x0d, 0xe4, 0x7a, 0x0e, 0xff, 0x43, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1a, 0xff, 0x39, 0x0e, - 0xff, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, - 0x6d, 0x00, 0x00, 0x00, 0xc9, 0x8e, 0x00, 0xfc, 0x5b, 0x00, 0x00, 0x00, - 0xd2, 0x84, 0x00, 0x00, 0x89, 0x79, 0x1c, 0xdd, 0x00, 0x00, 0x00, 0x93, - 0xad, 0xf9, 0x2e, 0x00, 0x00, 0xea, 0x6d, 0x00, 0x19, 0xe3, 0x8c, 0x00, - 0xab, 0xe1, 0x4f, 0x02, 0x07, 0x1f, 0x00, 0xea, 0x6d, 0x03, 0xb2, 0xc6, - 0x08, 0x00, 0xea, 0x6d, 0x03, 0x4d, 0xff, 0x33, 0x00, 0xea, 0x6d, 0x03, - 0x49, 0xff, 0x3a, 0x67, 0xee, 0x06, 0x00, 0x00, 0xd0, 0x8e, 0x00, 0xb3, - 0xbb, 0x00, 0x8e, 0xcf, 0x05, 0x0e, 0xff, 0x41, 0x12, 0x25, 0x00, 0x00, - 0x00, 0x00, 0x18, 0x20, 0x18, 0xff, 0x39, 0x0e, 0xff, 0x41, 0x2e, 0x33, - 0x01, 0x00, 0x03, 0x35, 0x01, 0x2e, 0x34, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x04, 0x39, 0x05, 0x00, 0x00, 0x03, 0x36, 0x02, 0x11, 0x25, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x6d, 0x00, 0x00, 0x00, - 0xc9, 0x8e, 0x00, 0xfc, 0x5b, 0x00, 0x00, 0x00, 0xd2, 0x84, 0x41, 0xd3, - 0xf0, 0xe3, 0xdd, 0xf4, 0xd3, 0x00, 0x06, 0xec, 0x52, 0xb4, 0x8d, 0x00, - 0x00, 0xea, 0x6d, 0x00, 0x06, 0xdd, 0x86, 0x0b, 0xfc, 0x81, 0x00, 0x00, - 0x00, 0x00, 0x00, 0xea, 0x6d, 0x8d, 0xdd, 0x15, 0x00, 0x00, 0xea, 0x6d, - 0x00, 0x07, 0xf3, 0x64, 0x00, 0xea, 0x6d, 0x00, 0x0a, 0xf4, 0x69, 0x13, - 0xfa, 0x4a, 0x00, 0x27, 0xff, 0x34, 0x00, 0x1c, 0xf1, 0x86, 0xf7, 0x33, - 0x00, 0x0e, 0xff, 0xaa, 0xd3, 0xf8, 0xab, 0x01, 0x00, 0x87, 0xf6, 0xda, - 0xa5, 0xff, 0x39, 0x0e, 0xff, 0xc0, 0xe5, 0xf2, 0xd5, 0x03, 0x0e, 0xff, - 0xb4, 0xe5, 0xf2, 0xd6, 0x04, 0x00, 0x00, 0x00, 0x00, 0x6d, 0xe7, 0xdd, - 0xe7, 0x6d, 0x00, 0x0e, 0xff, 0x98, 0xd2, 0xf8, 0xab, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0xea, 0xe9, 0xd9, 0xd9, 0xd9, 0xf7, 0x8e, 0x00, 0xfc, - 0x5b, 0x00, 0x00, 0x00, 0xd2, 0x84, 0x09, 0x1d, 0xec, 0x2e, 0x93, 0x87, - 0x1d, 0x00, 0x52, 0xf1, 0x09, 0x5d, 0xe8, 0x04, 0x00, 0xea, 0xe4, 0xd3, - 0xf6, 0xb1, 0x0d, 0x3c, 0xff, 0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, - 0xc9, 0xff, 0x60, 0x00, 0x00, 0x00, 0xea, 0x8a, 0x49, 0x9e, 0xf1, 0x0d, - 0x00, 0xea, 0xa6, 0x71, 0xbb, 0xe1, 0x1a, 0x00, 0xb4, 0xa0, 0x00, 0x7d, - 0xda, 0x00, 0x00, 0x00, 0x74, 0xff, 0x93, 0x00, 0x00, 0x0e, 0xff, 0x9c, - 0x02, 0x2a, 0xff, 0x32, 0x0e, 0xfa, 0x5a, 0x00, 0x6f, 0xff, 0x39, 0x0e, - 0xff, 0xa6, 0x04, 0x1e, 0xff, 0x42, 0x0e, 0xff, 0xa5, 0x04, 0x1e, 0xff, - 0x42, 0x00, 0x00, 0x00, 0x07, 0xf1, 0x59, 0x00, 0x55, 0xf2, 0x09, 0x0e, - 0xff, 0x99, 0x01, 0x2d, 0xff, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, - 0x98, 0x4c, 0x4c, 0x4c, 0xd9, 0x8e, 0x00, 0xfc, 0x5b, 0x00, 0x00, 0x00, - 0xd2, 0x84, 0x37, 0x58, 0xef, 0x4a, 0xc6, 0x83, 0x38, 0x00, 0xb1, 0xd5, - 0x6f, 0x7d, 0xff, 0x4c, 0x00, 0xea, 0x94, 0x47, 0x70, 0xe7, 0x83, 0x22, - 0xff, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0xd2, 0xaf, 0xe2, 0x10, - 0x00, 0x00, 0xea, 0xf5, 0xda, 0x94, 0x3e, 0x00, 0x00, 0xea, 0xd6, 0xc4, - 0xfd, 0x27, 0x00, 0x00, 0x5b, 0xef, 0x06, 0xd3, 0x81, 0x00, 0x00, 0x01, - 0xbc, 0xeb, 0xd2, 0x06, 0x00, 0x0e, 0xff, 0x56, 0x00, 0x00, 0xdd, 0x77, - 0x50, 0xfb, 0x0c, 0x00, 0x23, 0xff, 0x39, 0x0e, 0xff, 0x5c, 0x00, 0x00, - 0xeb, 0x68, 0x0e, 0xff, 0x5c, 0x00, 0x00, 0xeb, 0x68, 0x00, 0x00, 0x00, - 0x4c, 0xfb, 0x0b, 0x00, 0x08, 0xf8, 0x51, 0x0e, 0xff, 0x52, 0x00, 0x00, - 0xde, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x6d, 0x00, 0x00, 0x00, - 0xc9, 0x8e, 0x00, 0xd3, 0x85, 0x00, 0x00, 0x09, 0xf4, 0x6b, 0x7b, 0xc7, - 0xdf, 0xa7, 0xfd, 0xa9, 0x7f, 0x16, 0xfa, 0xc4, 0xb8, 0xb8, 0xe2, 0xab, - 0x00, 0xea, 0x6d, 0x00, 0x00, 0x8e, 0xdf, 0x00, 0xd2, 0x89, 0x00, 0x00, - 0x00, 0x00, 0x00, 0xea, 0x6d, 0x0f, 0xe0, 0xa1, 0x00, 0x00, 0xea, 0x6d, - 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x6d, 0x01, 0xc6, 0xa5, 0x00, 0x00, - 0x0c, 0xf5, 0x69, 0xff, 0x28, 0x00, 0x00, 0x60, 0xec, 0x21, 0xe4, 0x80, - 0x00, 0x0e, 0xff, 0x65, 0x00, 0x01, 0xea, 0x67, 0x43, 0xfe, 0x12, 0x00, - 0x25, 0xff, 0x39, 0x0e, 0xff, 0x43, 0x00, 0x00, 0xea, 0x68, 0x0e, 0xff, - 0x43, 0x00, 0x00, 0xea, 0x68, 0x00, 0x00, 0x00, 0x4c, 0xfe, 0x16, 0x00, - 0x11, 0xfd, 0x40, 0x0e, 0xff, 0x65, 0x00, 0x01, 0xeb, 0x67, 0x00, 0x00, - 0x00, 0x00, 0x00, 0xea, 0x6d, 0x00, 0x00, 0x00, 0xc9, 0x8e, 0x00, 0x72, - 0xdf, 0x4b, 0x23, 0x8c, 0xfa, 0x28, 0x00, 0x7c, 0x81, 0x1a, 0xe5, 0x00, - 0x00, 0x70, 0xeb, 0x05, 0x00, 0x00, 0x5c, 0xf8, 0x00, 0xea, 0x7f, 0x22, - 0x4a, 0xdd, 0x95, 0x00, 0x84, 0xed, 0x7c, 0x1d, 0x24, 0x32, 0x00, 0xea, - 0x6d, 0x00, 0x40, 0xfd, 0x52, 0x00, 0xea, 0x6d, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xea, 0x6d, 0x00, 0x31, 0xfc, 0x49, 0x00, 0x00, 0xa8, 0xe3, 0xcd, - 0x00, 0x00, 0x17, 0xed, 0x62, 0x00, 0x54, 0xf9, 0x2d, 0x0e, 0xff, 0xbc, - 0x1d, 0x5f, 0xff, 0x21, 0x07, 0xf5, 0x7e, 0x11, 0x8d, 0xff, 0x39, 0x0e, - 0xff, 0x43, 0x00, 0x00, 0xea, 0x68, 0x0e, 0xff, 0x43, 0x00, 0x00, 0xea, - 0x68, 0x00, 0x00, 0x00, 0x14, 0xf2, 0x88, 0x15, 0x82, 0xe8, 0x03, 0x0e, - 0xff, 0xbc, 0x1e, 0x61, 0xff, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, - 0x6d, 0x00, 0x00, 0x00, 0xc9, 0x8e, 0x00, 0x10, 0x85, 0xdc, 0xfe, 0xd9, - 0x4c, 0x00, 0x00, 0xaa, 0x50, 0x4b, 0xb4, 0x00, 0x00, 0xcf, 0x97, 0x00, - 0x00, 0x00, 0x0c, 0xf4, 0x00, 0xea, 0xff, 0xff, 0xdd, 0x93, 0x1c, 0x00, - 0x08, 0x5c, 0xc0, 0xfe, 0xed, 0x7b, 0x00, 0xea, 0x6d, 0x00, 0x00, 0x8d, - 0xea, 0x00, 0xea, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x6d, 0x00, - 0x00, 0x95, 0xe0, 0x00, 0x00, 0x4f, 0xff, 0x74, 0x00, 0x00, 0xa8, 0xc3, - 0x01, 0x00, 0x00, 0xb7, 0xca, 0x0e, 0xfd, 0x6f, 0xd4, 0xe5, 0x72, 0x00, - 0x00, 0x5c, 0xd9, 0xdb, 0x63, 0xef, 0x39, 0x0e, 0xff, 0x43, 0x00, 0x00, - 0xea, 0x68, 0x0e, 0xff, 0x43, 0x00, 0x00, 0xea, 0x68, 0x00, 0x00, 0x00, - 0x00, 0x43, 0xdb, 0xfc, 0xb7, 0x46, 0x00, 0x0e, 0xff, 0x8c, 0xd4, 0xe4, - 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x06, 0x00, 0x00, 0x00, 0x00, 0x02, 0x04, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, - 0x00, 0x00, 0x00, 0x0e, 0xff, 0x43, 0x01, 0x06, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, - 0xff, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0xc2, 0x33, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x79, - 0x00, 0x00, 0x00, 0x00, 0x58, 0x8a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x0a, 0x2f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2e, - 0x14, 0x00, 0x00, 0x04, 0x32, 0x3b, 0x03, 0x00, 0x00, 0x00, 0x21, 0x38, - 0x04, 0x00, 0x00, 0x00, 0x00, 0x07, 0x3b, 0x0f, 0x00, 0x2e, 0x35, 0x35, - 0x35, 0x16, 0x00, 0x00, 0x03, 0x26, 0x49, 0x1f, 0x1e, 0x33, 0x33, 0x33, - 0x33, 0x33, 0x00, 0x00, 0x18, 0x3a, 0x05, 0x00, 0x00, 0x00, 0x14, 0x37, - 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2f, - 0x00, 0x31, 0x35, 0x35, 0x35, 0x27, 0x00, 0x31, 0x35, 0x35, 0x35, 0x26, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, 0xa6, 0xea, 0x9e, 0x53, 0x0f, 0x03, - 0x5f, 0xa3, 0x00, 0x13, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6d, - 0xe9, 0xea, 0xb4, 0x23, 0x00, 0x07, 0x98, 0xff, 0x61, 0x00, 0x27, 0xd4, - 0xee, 0xe8, 0xe8, 0x2f, 0x32, 0xcf, 0xdd, 0xe5, 0xef, 0x33, 0x00, 0x00, - 0x00, 0x89, 0xff, 0x41, 0x00, 0xea, 0xf9, 0xf6, 0xf6, 0x64, 0x00, 0x25, - 0xcf, 0xfe, 0xd1, 0x6c, 0x90, 0xf6, 0xf6, 0xf6, 0xf9, 0xff, 0x00, 0xa0, - 0xea, 0xca, 0xee, 0x3e, 0x00, 0x9e, 0xf3, 0xe2, 0xe3, 0x1c, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xfc, 0x00, 0xea, 0xf4, 0xec, - 0xec, 0xac, 0x00, 0xea, 0xf4, 0xec, 0xec, 0xa9, 0x00, 0x00, 0x00, 0x00, - 0x17, 0x3a, 0x05, 0x30, 0x0c, 0x06, 0x36, 0x0c, 0x00, 0x00, 0x34, 0x13, - 0x16, 0xf6, 0x91, 0xc3, 0x79, 0x5b, 0x80, 0xed, 0xc9, 0xd9, 0xdb, 0xcb, - 0x00, 0x00, 0x31, 0xa9, 0x00, 0x00, 0x06, 0xf7, 0x57, 0x0c, 0xbd, 0x88, - 0x05, 0xc6, 0xb6, 0xed, 0x61, 0x00, 0x08, 0x64, 0x05, 0x02, 0xbf, 0x9d, - 0x04, 0x3a, 0x00, 0x01, 0xbe, 0xa2, 0x00, 0x00, 0x3d, 0xd1, 0xfc, 0x41, - 0x02, 0xfc, 0x41, 0x00, 0x00, 0x00, 0x00, 0xc9, 0xa9, 0x1f, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x90, 0xbf, 0x23, 0xfe, 0x27, 0x00, 0x9b, 0xaf, - 0x2b, 0xfc, 0x21, 0x00, 0xa9, 0xb1, 0x00, 0x00, 0x00, 0x01, 0x50, 0xce, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6f, 0x98, 0x1e, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x7c, 0xc1, 0x00, 0xea, 0x6d, 0x00, 0x00, 0x00, 0x00, 0xea, - 0x6a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x85, 0xf6, 0xdf, 0xbf, 0xf9, - 0x39, 0x1d, 0xff, 0x38, 0x00, 0x00, 0xf6, 0x5c, 0x30, 0xff, 0x55, 0xa7, - 0x00, 0x00, 0x06, 0x21, 0xdd, 0xeb, 0x45, 0x0c, 0x00, 0x00, 0x3a, 0xc8, - 0x00, 0x00, 0x35, 0xfd, 0x09, 0x00, 0x77, 0xc1, 0x00, 0x44, 0x03, 0xee, - 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa6, 0xa9, 0x00, 0x00, 0x00, 0x09, - 0xdb, 0x79, 0x00, 0x0d, 0xdc, 0x43, 0xff, 0x41, 0x13, 0xff, 0x38, 0x0f, - 0x00, 0x00, 0x36, 0xff, 0x34, 0x26, 0x05, 0x00, 0x00, 0x00, 0x00, 0x12, - 0xf3, 0x4e, 0x0a, 0xf1, 0x6b, 0x16, 0xce, 0x80, 0x6e, 0xda, 0x00, 0x00, - 0x58, 0xe9, 0x00, 0x01, 0x50, 0xce, 0xbc, 0x44, 0x49, 0xab, 0xab, 0xab, - 0xab, 0xa8, 0x0f, 0x78, 0xe1, 0x98, 0x1f, 0x00, 0x00, 0x01, 0xdb, 0x62, - 0x00, 0xea, 0x6d, 0x00, 0x00, 0x00, 0x00, 0xea, 0x6a, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x0e, 0xfa, 0x58, 0x00, 0x6f, 0xff, 0x39, 0x1d, 0xff, 0x38, - 0x00, 0x00, 0xf6, 0x5c, 0x00, 0x90, 0xfc, 0xdd, 0x46, 0x00, 0x00, 0x8b, - 0xbb, 0x75, 0xce, 0x06, 0x41, 0x8d, 0xa7, 0xe7, 0x8d, 0x8d, 0x6b, 0xe3, - 0x00, 0x00, 0x52, 0xf5, 0x00, 0x00, 0x00, 0xf0, 0x61, 0x00, 0x00, 0x00, - 0x00, 0x31, 0xf8, 0x3f, 0x00, 0x33, 0xcb, 0xf8, 0x85, 0x05, 0x00, 0x9c, - 0x8e, 0x08, 0xff, 0x41, 0x1f, 0xfd, 0xfe, 0xfa, 0xb8, 0x2d, 0x5b, 0xf2, - 0xc9, 0xe6, 0xe7, 0x5a, 0x00, 0x00, 0x00, 0x7c, 0xd9, 0x02, 0x00, 0x37, - 0xfb, 0xf8, 0xa4, 0x02, 0x2e, 0xfd, 0x49, 0x26, 0xbd, 0xfe, 0x35, 0xce, - 0xac, 0x34, 0x00, 0x00, 0x24, 0x55, 0x55, 0x55, 0x55, 0x53, 0x00, 0x00, - 0x08, 0x68, 0xd8, 0x98, 0x00, 0x3b, 0xf4, 0x0d, 0x00, 0xea, 0xe8, 0xd6, - 0xd6, 0x6f, 0x00, 0xea, 0xb6, 0x82, 0x82, 0x42, 0x00, 0x00, 0x4f, 0xfb, - 0x0c, 0x00, 0x23, 0xff, 0x39, 0x1d, 0xff, 0x38, 0x00, 0x00, 0xf6, 0x5c, - 0x00, 0x00, 0x3a, 0xe4, 0xf4, 0x89, 0x00, 0x24, 0x30, 0x0c, 0x48, 0x00, - 0x34, 0x72, 0x92, 0xe1, 0x72, 0x72, 0x5e, 0xea, 0x00, 0x00, 0x59, 0xee, - 0x00, 0x00, 0x00, 0xf0, 0x61, 0x00, 0x00, 0x00, 0x22, 0xe6, 0x76, 0x00, - 0x00, 0x15, 0x57, 0x82, 0xe7, 0x79, 0x4d, 0xd5, 0x09, 0x08, 0xff, 0x41, - 0x00, 0x13, 0x03, 0x2c, 0xcd, 0xa5, 0x6f, 0xfc, 0x30, 0x01, 0x81, 0xd3, - 0x00, 0x00, 0x09, 0xe9, 0x6a, 0x00, 0x0f, 0xc5, 0xb2, 0x78, 0xed, 0x63, - 0x00, 0x7f, 0xe4, 0xe4, 0xa5, 0xeb, 0x34, 0xc4, 0xcd, 0x64, 0x0a, 0x00, - 0x2e, 0x6c, 0x6c, 0x6c, 0x6c, 0x6a, 0x00, 0x00, 0x29, 0x92, 0xe4, 0x93, - 0x00, 0x9a, 0xa3, 0x00, 0x00, 0xea, 0x96, 0x48, 0x48, 0x25, 0x00, 0xea, - 0xc8, 0xa1, 0xa1, 0x52, 0x00, 0x00, 0x43, 0xfe, 0x12, 0x00, 0x23, 0xff, - 0x39, 0x1c, 0xff, 0x3d, 0x00, 0x1c, 0xff, 0x5c, 0x06, 0x00, 0x17, 0xa7, - 0x8a, 0xdd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3a, 0xc8, - 0x00, 0x00, 0x25, 0xff, 0x0f, 0x00, 0x7f, 0xb8, 0x00, 0x00, 0x00, 0xf0, - 0x61, 0x00, 0x00, 0x20, 0xe0, 0x7d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x84, 0xe4, 0xc4, 0xf0, 0xe2, 0xe3, 0xff, 0xe9, 0x00, 0x00, 0x00, 0x00, - 0x7b, 0xdd, 0x4b, 0xf3, 0x07, 0x00, 0x47, 0xf8, 0x00, 0x00, 0x68, 0xed, - 0x0b, 0x00, 0x5f, 0xe3, 0x02, 0x00, 0x5d, 0xeb, 0x00, 0x00, 0x01, 0x00, - 0xaa, 0xbd, 0x00, 0x00, 0x35, 0xa5, 0xea, 0x90, 0x3f, 0x92, 0x92, 0x92, - 0x92, 0x90, 0x34, 0xbe, 0xe0, 0x74, 0x10, 0x00, 0x09, 0xf0, 0x44, 0x00, - 0x00, 0xea, 0x6d, 0x00, 0x00, 0x00, 0x00, 0xea, 0x6a, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x07, 0xf5, 0x7b, 0x10, 0x8b, 0xff, 0x39, 0x02, 0xe4, 0x8c, - 0x10, 0x86, 0xff, 0x5c, 0x60, 0xd2, 0xa9, 0xea, 0xfd, 0x77, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0x91, 0x00, 0x00, 0x01, 0xea, - 0x87, 0x2f, 0xd5, 0x81, 0x00, 0x00, 0x00, 0xf0, 0x61, 0x00, 0x1f, 0xdf, - 0x9f, 0x29, 0x29, 0x29, 0x36, 0x28, 0x04, 0x29, 0xcd, 0x9c, 0x2a, 0x35, - 0x35, 0x3b, 0xff, 0x69, 0x28, 0x37, 0x09, 0x32, 0xd3, 0x81, 0x16, 0xef, - 0x7f, 0x18, 0xa7, 0xaf, 0x00, 0x03, 0xdb, 0x87, 0x00, 0x00, 0x3e, 0xf5, - 0x26, 0x05, 0x88, 0xc7, 0x00, 0x06, 0x1b, 0x75, 0xf4, 0x48, 0x00, 0x00, - 0x00, 0x00, 0x1b, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4d, 0x56, - 0x04, 0x00, 0x00, 0x00, 0x5a, 0xe2, 0x02, 0x00, 0x00, 0xea, 0x81, 0x24, - 0x24, 0x1a, 0x00, 0xea, 0x6a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5c, - 0xda, 0xda, 0x71, 0xff, 0x39, 0x00, 0x68, 0xe4, 0xfa, 0xa6, 0xd0, 0x5c, - 0x09, 0x38, 0x74, 0xc9, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0xb5, 0xef, 0x82, 0x10, - 0x00, 0x00, 0x00, 0xf0, 0x61, 0x00, 0x7e, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x5f, 0xdc, 0xfc, 0xe9, 0xa6, 0x22, 0x00, 0x00, 0x00, 0x08, 0xff, 0x41, - 0x3e, 0xd7, 0xfd, 0xe4, 0x95, 0x17, 0x00, 0x47, 0xdc, 0xfb, 0xb2, 0x30, - 0x00, 0x55, 0xfa, 0x1b, 0x00, 0x00, 0x00, 0x90, 0xe3, 0xf2, 0xbf, 0x3c, - 0x00, 0xec, 0xfc, 0xde, 0x57, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xb9, 0x85, 0x00, 0x00, 0x00, 0xea, 0xff, 0xff, 0xff, 0xba, 0x00, 0xea, - 0x6a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x04, 0x1a, 0xff, - 0x39, 0x00, 0x00, 0x02, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x70, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x0f, 0x0d, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1a, 0xff, 0x39, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x14, 0xc2, 0x2b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0x17, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x15, 0x43, 0x14, 0x00, 0x2d, 0x35, 0x35, 0x35, - 0x35, 0x35, 0x32, 0x1b, 0x00, 0x00, 0x00, 0x26, 0x1c, 0x35, 0x35, 0x35, - 0x35, 0x35, 0x00, 0x00, 0x1d, 0x17, 0x00, 0x00, 0x00, 0x00, 0x8c, 0xa9, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0xbc, 0x2f, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x7a, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0x35, 0x35, - 0x35, 0x2b, 0x0d, 0x35, 0x04, 0x35, 0x0e, 0x00, 0x12, 0x41, 0x0e, 0x00, - 0x2f, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x6d, 0x00, 0x00, 0x00, 0x00, 0xa1, - 0xf9, 0xe4, 0xfe, 0x7c, 0xc9, 0xf0, 0xf6, 0xfe, 0xf0, 0xf0, 0x9e, 0xd1, - 0x02, 0x00, 0x14, 0xf1, 0x7c, 0xf0, 0xf0, 0xf0, 0xf8, 0xff, 0x00, 0x00, - 0xbd, 0xae, 0x00, 0x00, 0x00, 0x00, 0x0b, 0xb2, 0x74, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x0e, 0xff, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x58, 0xaa, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0xb5, 0x60, 0x60, 0x60, 0xaa, 0x33, 0xfc, - 0x02, 0xf9, 0x36, 0x9f, 0xeb, 0xd6, 0xf7, 0x5d, 0xa5, 0x99, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xea, 0x6d, 0x00, 0x00, 0x00, 0x2b, 0xfe, 0x2d, 0x00, 0x1a, 0x15, - 0x00, 0x00, 0x6c, 0xea, 0x00, 0x00, 0x1d, 0xf7, 0x58, 0x00, 0x8a, 0xda, - 0x00, 0x00, 0x00, 0x08, 0xd5, 0x9c, 0x00, 0x35, 0xc9, 0xd2, 0x34, 0x00, - 0x00, 0x00, 0x00, 0x01, 0x06, 0x00, 0x00, 0x03, 0x35, 0x42, 0x09, 0x00, - 0x00, 0x00, 0x07, 0x38, 0x03, 0x00, 0x00, 0x00, 0x28, 0x48, 0x36, 0x36, - 0x0e, 0xff, 0x40, 0x00, 0x0e, 0x36, 0x34, 0x17, 0x00, 0x00, 0x0f, 0x36, - 0x25, 0x2c, 0x00, 0x00, 0x1a, 0x35, 0x33, 0x1a, 0x00, 0x00, 0x10, 0x36, - 0x00, 0x00, 0x58, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x95, 0x00, 0x00, 0x00, 0x95, 0x1f, 0xea, 0x00, 0xe6, 0x22, 0x1c, - 0x04, 0x00, 0x7a, 0xcf, 0x46, 0xef, 0x09, 0x00, 0x00, 0x00, 0x04, 0x3e, - 0x21, 0x00, 0x04, 0x36, 0x34, 0x06, 0x00, 0x00, 0x00, 0xea, 0x6d, 0x00, - 0x00, 0x00, 0x24, 0xff, 0x4c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6c, 0xea, - 0x00, 0x00, 0x00, 0x8b, 0xd9, 0x1d, 0xf5, 0x54, 0x00, 0x00, 0x00, 0x8b, - 0xe0, 0x0e, 0x00, 0xac, 0x58, 0x54, 0xb7, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0xa7, 0xd5, 0xc6, 0xf3, 0x42, 0x00, 0x6d, 0xe1, 0xc7, - 0xe1, 0x49, 0x02, 0xca, 0xbd, 0xb6, 0xff, 0xb2, 0x0e, 0xff, 0x40, 0x0c, - 0xc7, 0xae, 0xba, 0xa4, 0x00, 0x00, 0x7f, 0xdf, 0x46, 0xfb, 0x38, 0x08, - 0xd8, 0x97, 0xb6, 0xb1, 0x00, 0x00, 0x81, 0xe2, 0x00, 0x00, 0x58, 0xaa, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x95, 0x00, 0x00, - 0x00, 0x95, 0x0a, 0xa3, 0x00, 0xa1, 0x0d, 0x00, 0x00, 0x02, 0xa9, 0xb9, - 0x03, 0xe3, 0x58, 0x00, 0x00, 0x70, 0xe7, 0xdd, 0xf7, 0x11, 0xe9, 0xe3, - 0xd8, 0xcf, 0x00, 0x00, 0x00, 0xea, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x84, - 0xfb, 0xb6, 0x33, 0x00, 0x00, 0x00, 0x6c, 0xea, 0x00, 0x00, 0x00, 0x12, - 0xef, 0xd9, 0xc8, 0x01, 0x00, 0x00, 0x3b, 0xfb, 0x42, 0x00, 0x26, 0xdf, - 0x04, 0x02, 0xd1, 0x3d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, - 0x00, 0x00, 0xa7, 0xa5, 0x07, 0xf1, 0x3f, 0x00, 0x77, 0xba, 0x45, 0xf4, - 0x06, 0x01, 0xe9, 0x66, 0x0e, 0xff, 0x45, 0xbc, 0xbb, 0x07, 0x59, 0xf3, - 0x0a, 0x00, 0xd8, 0x80, 0x00, 0x95, 0xd5, 0x8f, 0xdb, 0x0b, 0x50, 0xf9, - 0x13, 0x00, 0xd8, 0x85, 0x00, 0x00, 0x58, 0xaa, 0x00, 0x00, 0x2c, 0xb1, - 0xcf, 0x88, 0x4c, 0x89, 0x00, 0x95, 0x00, 0x00, 0x00, 0x95, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x9a, 0xd6, 0x1d, 0x00, 0x86, 0xb7, 0x00, - 0x07, 0xf1, 0x62, 0x00, 0x06, 0x58, 0xf8, 0x20, 0x00, 0x08, 0x00, 0x00, - 0x00, 0xea, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0xbe, 0xfe, 0x5a, - 0x00, 0x00, 0x6c, 0xea, 0x00, 0x00, 0x00, 0x00, 0x79, 0xff, 0x40, 0x00, - 0x00, 0x0a, 0xda, 0x92, 0x00, 0x00, 0x9a, 0x75, 0x00, 0x00, 0x53, 0xc0, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x8b, 0xbf, 0xe6, 0xc5, - 0x49, 0xfe, 0xc3, 0xc2, 0xd2, 0xfc, 0x36, 0xfe, 0x39, 0x2b, 0xf8, 0x29, - 0x0e, 0xff, 0xc7, 0xf9, 0x18, 0x00, 0x09, 0xef, 0x57, 0x32, 0xfd, 0x20, - 0x00, 0x0b, 0xe0, 0xff, 0x3d, 0x00, 0x04, 0xe5, 0x69, 0x31, 0xfe, 0x25, - 0x00, 0x00, 0x58, 0xaa, 0x00, 0x00, 0x49, 0x3a, 0x47, 0x91, 0xbd, 0x67, - 0x00, 0x95, 0x00, 0x00, 0x00, 0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x43, 0xe6, 0x15, 0x00, 0x00, 0x28, 0xfb, 0x1a, 0x4a, 0xfb, 0x0d, 0x00, - 0x00, 0x11, 0xc2, 0xf4, 0x90, 0x19, 0x00, 0x00, 0x00, 0xea, 0x6d, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xaf, 0xbe, 0x00, 0x00, 0x6c, 0xea, - 0x00, 0x00, 0x00, 0x00, 0x47, 0xff, 0x12, 0x00, 0x00, 0x91, 0xda, 0x0a, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x27, 0xfc, 0x6a, 0x1e, 0x9a, 0xc5, 0x3b, 0xfd, 0x42, 0x31, - 0x31, 0x31, 0x00, 0x9f, 0xe5, 0xb3, 0x5d, 0x00, 0x0e, 0xff, 0xac, 0xd2, - 0xa4, 0x00, 0x00, 0x97, 0xac, 0x88, 0xbc, 0x00, 0x00, 0x2b, 0xf7, 0xee, - 0x75, 0x00, 0x00, 0x82, 0xbf, 0x86, 0xc3, 0x00, 0x00, 0x00, 0x58, 0xaa, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x95, 0x00, 0x00, - 0x00, 0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x5b, 0x00, 0x00, - 0x00, 0x00, 0xc7, 0x76, 0x40, 0xfe, 0x15, 0x00, 0x00, 0x00, 0x01, 0x4f, - 0xd1, 0xdf, 0x00, 0x00, 0x00, 0xea, 0x83, 0x27, 0x27, 0x23, 0x39, 0x44, - 0x14, 0x2b, 0xd1, 0x7b, 0x00, 0x00, 0x6c, 0xea, 0x00, 0x00, 0x00, 0x00, - 0x47, 0xff, 0x12, 0x00, 0x40, 0xfd, 0x60, 0x27, 0x27, 0x27, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5f, 0xf6, - 0x0f, 0x1f, 0xe4, 0xc5, 0x01, 0xe0, 0x89, 0x0e, 0x07, 0x31, 0x08, 0xfa, - 0x4e, 0x24, 0x0a, 0x00, 0x0e, 0xff, 0x40, 0x2b, 0xf5, 0x65, 0x00, 0x36, - 0xef, 0xd1, 0x5b, 0x00, 0x05, 0xcd, 0x9e, 0x4b, 0xf8, 0x2d, 0x00, 0x1e, - 0xf6, 0xda, 0x63, 0x00, 0x00, 0x00, 0x58, 0xaa, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x95, 0x00, 0x00, 0x00, 0x95, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x10, 0x5f, 0x00, 0x00, 0x00, 0x00, 0x68, 0xd4, - 0x04, 0xec, 0x8a, 0x14, 0x3f, 0x27, 0x2c, 0x01, 0x54, 0xee, 0x00, 0x00, - 0x00, 0xea, 0xff, 0xff, 0xff, 0xe6, 0x51, 0xdb, 0xfd, 0xea, 0xa3, 0x11, - 0x00, 0x00, 0x6c, 0xea, 0x00, 0x00, 0x00, 0x00, 0x47, 0xff, 0x12, 0x00, - 0xa0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xc8, 0xf7, 0xdf, 0x83, 0xc5, - 0x00, 0x3f, 0xae, 0xf4, 0xfc, 0x9f, 0x0a, 0xd2, 0xfc, 0xfb, 0xf7, 0x90, - 0x0e, 0xff, 0x40, 0x00, 0x65, 0xf6, 0x00, 0x00, 0xd4, 0xf0, 0x09, 0x00, - 0x83, 0xe5, 0x10, 0x00, 0xa2, 0xd0, 0x00, 0x00, 0xb4, 0xf4, 0x0d, 0x00, - 0x00, 0x00, 0x58, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xc6, 0x95, 0x95, 0x95, 0xb6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x82, 0xe8, 0x00, 0x00, 0x00, 0x00, 0x11, 0xf7, 0x00, 0x4f, 0xbf, 0xfe, - 0xf1, 0x57, 0xe9, 0xf3, 0xde, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x0c, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, - 0x0a, 0x00, 0xaa, 0x94, 0x08, 0x02, 0x4e, 0xf8, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x06, 0xbf, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x58, 0xaa, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x0f, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x05, 0x00, 0x00, 0x0f, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa0, 0xae, - 0x37, 0x49, 0xae, 0x9e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0xc3, - 0xf1, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x58, 0xaa, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x79, 0xb0, 0x96, 0x5c, 0x10, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x89, 0x9f, 0x3a, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x3d, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5a, 0xff, 0x5a, 0xff, 0x8f, 0x00, - 0x00, 0x00, 0xff, 0x8f, 0x00, 0x00, 0x00, 0x8f, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x3c, 0xaa, 0x3c, 0x8f, 0xff, 0x8f, 0x00, 0x00, 0x8f, 0xff, - 0x8f, 0x00, 0x8f, 0xff, 0x8f, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x8f, 0xff, 0x8f, 0x00, 0x00, 0x8f, 0xff, 0x8f, 0xff, 0x8f, - 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, - 0x5c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5c, 0x14, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x6e, 0xaa, 0x00, 0x3c, 0xaa, 0x3c, 0x00, 0x00, 0x8f, - 0xff, 0x8f, 0x00, 0x00, 0x8f, 0xff, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x86, 0xbd, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x17, - 0x00, 0x00, 0x00, 0x0a, 0x35, 0x13, 0x00, 0x00, 0x30, 0x20, 0x1d, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x31, 0x17, 0x00, 0x33, 0x15, 0x07, 0x35, 0x35, - 0x26, 0x35, 0x31, 0x03, 0x7f, 0x18, 0x03, 0x7f, 0x18, 0x0b, 0xbc, 0x31, - 0x00, 0x00, 0x0d, 0x35, 0x00, 0x00, 0x00, 0x5e, 0xff, 0x70, 0x00, 0x00, - 0x00, 0x00, 0x70, 0xff, 0x5e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa5, - 0xff, 0x00, 0x5a, 0xff, 0x5a, 0x00, 0x8f, 0xff, 0x8f, 0x00, 0x00, 0x00, - 0x00, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x81, 0xd8, 0x56, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x4f, 0x3b, 0x00, 0x00, 0x00, 0xb5, 0xf5, 0xb0, 0xe7, 0x28, 0x00, 0x26, - 0xff, 0x55, 0x00, 0x3e, 0xe7, 0x4c, 0xdc, 0x06, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xea, 0x6d, 0x00, 0xf4, 0x64, 0x22, 0xff, 0xcb, 0x8e, 0xd6, 0xeb, 0x0b, - 0xc5, 0x2f, 0x0b, 0xc5, 0x2f, 0x0e, 0xff, 0x43, 0x00, 0x00, 0x33, 0xfc, - 0x00, 0x00, 0x00, 0x00, 0x70, 0xff, 0x70, 0x00, 0x00, 0x70, 0xff, 0x70, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x8f, 0xff, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x17, 0x36, 0x36, 0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x07, - 0xcd, 0xa8, 0x36, 0x03, 0x35, 0x00, 0x24, 0x0b, 0xcf, 0x85, 0x36, 0x00, - 0x32, 0xfd, 0x1b, 0x01, 0xbb, 0x85, 0x00, 0x19, 0xff, 0x48, 0x01, 0xca, - 0x70, 0x01, 0xca, 0x6f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x2c, 0x09, 0x00, 0x2c, 0x08, 0x00, 0xea, 0x6d, 0x00, 0xf4, - 0x64, 0x22, 0xff, 0x12, 0x00, 0x46, 0xeb, 0x03, 0x36, 0x0e, 0x03, 0x36, - 0x0e, 0x0e, 0xff, 0x43, 0x00, 0x00, 0x1f, 0xea, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x70, 0xff, 0x70, 0x70, 0xff, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x55, 0x37, 0x00, 0x37, 0x55, 0x00, 0x1e, 0x55, 0x1e, 0xff, 0x8f, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x57, 0xcc, - 0xcc, 0xe8, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x98, 0xf3, 0xe6, 0xc9, 0x0e, - 0xff, 0x85, 0xfc, 0x93, 0xfb, 0xde, 0xc9, 0x00, 0x41, 0xf6, 0x00, 0x00, - 0x9e, 0x99, 0x00, 0x0c, 0xff, 0x3c, 0x2c, 0xff, 0x0f, 0x00, 0x69, 0xd0, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0xf8, 0x54, - 0x20, 0xf8, 0x51, 0x00, 0xea, 0x6d, 0x00, 0xf4, 0x64, 0x22, 0xff, 0x12, - 0x00, 0x46, 0xeb, 0x0e, 0xff, 0x43, 0x0e, 0xff, 0x43, 0x0e, 0xff, 0x43, - 0x00, 0x00, 0x0a, 0xa3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0xff, - 0xff, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xa5, 0x00, 0xa5, - 0xff, 0x00, 0x5a, 0xff, 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0xe6, 0x6f, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xc7, 0x8b, 0x00, 0x0e, 0xff, 0xc7, 0x1f, 0x00, - 0xee, 0x64, 0x00, 0x00, 0x64, 0xe7, 0x00, 0x00, 0x8f, 0xba, 0x00, 0x01, - 0xfd, 0x2f, 0x54, 0xe5, 0x00, 0x00, 0x41, 0xf6, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x45, 0x09, 0x03, 0x46, 0x09, 0x00, - 0xea, 0x6d, 0x00, 0xf4, 0x64, 0x22, 0xff, 0x12, 0x00, 0x46, 0xeb, 0x0e, - 0xff, 0x43, 0x0e, 0xff, 0x43, 0x0e, 0xff, 0x43, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0xff, 0xff, 0x70, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x55, 0x37, 0x00, 0x37, 0x55, 0x00, 0x1e, 0x55, - 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, 0xb1, 0xb6, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xc7, 0x8b, 0x00, 0x0e, 0xff, 0x5d, 0x00, 0x00, 0xee, 0x64, 0x00, 0x9b, - 0xe8, 0x53, 0x00, 0x00, 0x23, 0xc4, 0xd7, 0x00, 0xf1, 0x22, 0x7b, 0xbe, - 0x00, 0x00, 0x1a, 0xff, 0x00, 0x00, 0x00, 0x62, 0xa4, 0xa4, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x6d, 0x00, 0xf4, - 0x64, 0x22, 0xff, 0x12, 0x00, 0x46, 0xeb, 0x0e, 0xff, 0x43, 0x0e, 0xff, - 0x43, 0x0e, 0xff, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x70, 0xff, 0x70, 0x70, 0xff, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x69, - 0xe8, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc7, 0x8b, 0x00, 0x0e, - 0xff, 0x43, 0x00, 0x00, 0xee, 0x64, 0x00, 0x52, 0xcc, 0xa7, 0x00, 0x00, - 0x4e, 0xea, 0x76, 0x00, 0x75, 0x0c, 0x6f, 0xca, 0x00, 0x00, 0x26, 0xff, - 0x00, 0x00, 0x00, 0x49, 0x7a, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xea, 0x6d, 0x00, 0xf4, 0x64, 0x22, 0xff, 0x12, - 0x00, 0x46, 0xeb, 0x0e, 0xff, 0x43, 0x0e, 0xff, 0x43, 0x0e, 0xff, 0x43, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0xff, 0x70, 0x00, - 0x00, 0x70, 0xff, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0xf5, 0x4c, 0x03, 0x03, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xc7, 0x8b, 0x00, 0x0e, 0xff, 0x43, 0x00, 0x00, - 0xd0, 0x8f, 0x05, 0x00, 0x4a, 0xf5, 0x00, 0x00, 0x99, 0xa2, 0x00, 0x08, - 0x75, 0x17, 0x47, 0xf2, 0x01, 0x00, 0x4e, 0xeb, 0x09, 0x56, 0x17, 0x00, - 0x00, 0x00, 0x08, 0x75, 0x17, 0x08, 0x75, 0x17, 0x09, 0x56, 0x17, 0x00, - 0xea, 0x6d, 0x00, 0xf4, 0x64, 0x22, 0xff, 0x12, 0x00, 0x46, 0xeb, 0x0e, - 0xff, 0x43, 0x0e, 0xff, 0x43, 0x0e, 0xff, 0x43, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x5e, 0xff, 0x70, 0x00, 0x00, 0x00, 0x00, 0x70, 0xff, - 0x5e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x9a, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xc7, 0x8b, 0x00, 0x0e, 0xff, 0x43, 0x00, 0x00, 0x62, 0xee, 0xfc, 0x00, - 0x41, 0xf7, 0x00, 0x00, 0x9f, 0x99, 0x00, 0x27, 0xfe, 0x4f, 0x1a, 0xfc, - 0x27, 0x00, 0x82, 0xba, 0x3d, 0xf9, 0x14, 0x00, 0x00, 0x00, 0x27, 0xfe, - 0x4f, 0x27, 0xfe, 0x4f, 0x3d, 0xf9, 0x14, 0x00, 0xea, 0x6d, 0x00, 0xf6, - 0x60, 0x22, 0xff, 0x12, 0x00, 0x46, 0xeb, 0x0e, 0xff, 0x43, 0x0e, 0xff, - 0x43, 0x0e, 0xff, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, - 0x5c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5c, 0x14, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x0e, 0x00, 0x13, 0xf7, 0x74, 0x3a, - 0xe1, 0x6d, 0x00, 0x00, 0x12, 0x00, 0x00, 0x97, 0x9e, 0x0d, 0xeb, 0x3d, - 0x71, 0xaf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x12, 0x00, - 0x71, 0xaf, 0x00, 0x00, 0x00, 0x00, 0x33, 0xff, 0x25, 0x22, 0xff, 0x3c, - 0x20, 0x67, 0xeb, 0x00, 0x00, 0x00, 0x17, 0xff, 0x42, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc1, - 0xc1, 0xc1, 0xc1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x58, 0xa8, 0x8b, 0x86, 0x0d, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x16, 0xc8, 0x5c, 0x97, 0x00, 0x49, 0x33, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49, 0x33, 0x00, 0x00, - 0x00, 0x00, 0xf9, 0xad, 0x00, 0x1c, 0xd1, 0xd1, 0x95, 0xd1, 0xc0, 0x00, - 0x00, 0x00, 0xba, 0xf7, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -}; - - -static mu_Rect atlas[] = { - [ MU_ICON_CLOSE ] = { 88, 68, 16, 16 }, - [ MU_ICON_CHECK ] = { 0, 0, 18, 18 }, - [ MU_ICON_EXPANDED ] = { 118, 68, 7, 5 }, - [ MU_ICON_COLLAPSED ] = { 113, 68, 5, 7 }, - [ ATLAS_WHITE ] = { 125, 68, 3, 3 }, - [ ATLAS_FONT+32 ] = { 84, 68, 2, 17 }, - [ ATLAS_FONT+33 ] = { 39, 68, 3, 17 }, - [ ATLAS_FONT+34 ] = { 114, 51, 5, 17 }, - [ ATLAS_FONT+35 ] = { 34, 17, 7, 17 }, - [ ATLAS_FONT+36 ] = { 28, 34, 6, 17 }, - [ ATLAS_FONT+37 ] = { 58, 0, 9, 17 }, - [ ATLAS_FONT+38 ] = { 103, 0, 8, 17 }, - [ ATLAS_FONT+39 ] = { 86, 68, 2, 17 }, - [ ATLAS_FONT+40 ] = { 42, 68, 3, 17 }, - [ ATLAS_FONT+41 ] = { 45, 68, 3, 17 }, - [ ATLAS_FONT+42 ] = { 34, 34, 6, 17 }, - [ ATLAS_FONT+43 ] = { 40, 34, 6, 17 }, - [ ATLAS_FONT+44 ] = { 48, 68, 3, 17 }, - [ ATLAS_FONT+45 ] = { 51, 68, 3, 17 }, - [ ATLAS_FONT+46 ] = { 54, 68, 3, 17 }, - [ ATLAS_FONT+47 ] = { 124, 34, 4, 17 }, - [ ATLAS_FONT+48 ] = { 46, 34, 6, 17 }, - [ ATLAS_FONT+49 ] = { 52, 34, 6, 17 }, - [ ATLAS_FONT+50 ] = { 58, 34, 6, 17 }, - [ ATLAS_FONT+51 ] = { 64, 34, 6, 17 }, - [ ATLAS_FONT+52 ] = { 70, 34, 6, 17 }, - [ ATLAS_FONT+53 ] = { 76, 34, 6, 17 }, - [ ATLAS_FONT+54 ] = { 82, 34, 6, 17 }, - [ ATLAS_FONT+55 ] = { 88, 34, 6, 17 }, - [ ATLAS_FONT+56 ] = { 94, 34, 6, 17 }, - [ ATLAS_FONT+57 ] = { 100, 34, 6, 17 }, - [ ATLAS_FONT+58 ] = { 57, 68, 3, 17 }, - [ ATLAS_FONT+59 ] = { 60, 68, 3, 17 }, - [ ATLAS_FONT+60 ] = { 106, 34, 6, 17 }, - [ ATLAS_FONT+61 ] = { 112, 34, 6, 17 }, - [ ATLAS_FONT+62 ] = { 118, 34, 6, 17 }, - [ ATLAS_FONT+63 ] = { 119, 51, 5, 17 }, - [ ATLAS_FONT+64 ] = { 18, 0, 10, 17 }, - [ ATLAS_FONT+65 ] = { 41, 17, 7, 17 }, - [ ATLAS_FONT+66 ] = { 48, 17, 7, 17 }, - [ ATLAS_FONT+67 ] = { 55, 17, 7, 17 }, - [ ATLAS_FONT+68 ] = { 111, 0, 8, 17 }, - [ ATLAS_FONT+69 ] = { 0, 35, 6, 17 }, - [ ATLAS_FONT+70 ] = { 6, 35, 6, 17 }, - [ ATLAS_FONT+71 ] = { 119, 0, 8, 17 }, - [ ATLAS_FONT+72 ] = { 18, 17, 8, 17 }, - [ ATLAS_FONT+73 ] = { 63, 68, 3, 17 }, - [ ATLAS_FONT+74 ] = { 66, 68, 3, 17 }, - [ ATLAS_FONT+75 ] = { 62, 17, 7, 17 }, - [ ATLAS_FONT+76 ] = { 12, 51, 6, 17 }, - [ ATLAS_FONT+77 ] = { 28, 0, 10, 17 }, - [ ATLAS_FONT+78 ] = { 67, 0, 9, 17 }, - [ ATLAS_FONT+79 ] = { 76, 0, 9, 17 }, - [ ATLAS_FONT+80 ] = { 69, 17, 7, 17 }, - [ ATLAS_FONT+81 ] = { 85, 0, 9, 17 }, - [ ATLAS_FONT+82 ] = { 76, 17, 7, 17 }, - [ ATLAS_FONT+83 ] = { 18, 51, 6, 17 }, - [ ATLAS_FONT+84 ] = { 24, 51, 6, 17 }, - [ ATLAS_FONT+85 ] = { 26, 17, 8, 17 }, - [ ATLAS_FONT+86 ] = { 83, 17, 7, 17 }, - [ ATLAS_FONT+87 ] = { 38, 0, 10, 17 }, - [ ATLAS_FONT+88 ] = { 90, 17, 7, 17 }, - [ ATLAS_FONT+89 ] = { 30, 51, 6, 17 }, - [ ATLAS_FONT+90 ] = { 36, 51, 6, 17 }, - [ ATLAS_FONT+91 ] = { 69, 68, 3, 17 }, - [ ATLAS_FONT+92 ] = { 124, 51, 4, 17 }, - [ ATLAS_FONT+93 ] = { 72, 68, 3, 17 }, - [ ATLAS_FONT+94 ] = { 42, 51, 6, 17 }, - [ ATLAS_FONT+95 ] = { 15, 68, 4, 17 }, - [ ATLAS_FONT+96 ] = { 48, 51, 6, 17 }, - [ ATLAS_FONT+97 ] = { 54, 51, 6, 17 }, - [ ATLAS_FONT+98 ] = { 97, 17, 7, 17 }, - [ ATLAS_FONT+99 ] = { 0, 52, 5, 17 }, - [ ATLAS_FONT+100 ] = { 104, 17, 7, 17 }, - [ ATLAS_FONT+101 ] = { 60, 51, 6, 17 }, - [ ATLAS_FONT+102 ] = { 19, 68, 4, 17 }, - [ ATLAS_FONT+103 ] = { 66, 51, 6, 17 }, - [ ATLAS_FONT+104 ] = { 111, 17, 7, 17 }, - [ ATLAS_FONT+105 ] = { 75, 68, 3, 17 }, - [ ATLAS_FONT+106 ] = { 78, 68, 3, 17 }, - [ ATLAS_FONT+107 ] = { 72, 51, 6, 17 }, - [ ATLAS_FONT+108 ] = { 81, 68, 3, 17 }, - [ ATLAS_FONT+109 ] = { 48, 0, 10, 17 }, - [ ATLAS_FONT+110 ] = { 118, 17, 7, 17 }, - [ ATLAS_FONT+111 ] = { 0, 18, 7, 17 }, - [ ATLAS_FONT+112 ] = { 7, 18, 7, 17 }, - [ ATLAS_FONT+113 ] = { 14, 34, 7, 17 }, - [ ATLAS_FONT+114 ] = { 23, 68, 4, 17 }, - [ ATLAS_FONT+115 ] = { 5, 52, 5, 17 }, - [ ATLAS_FONT+116 ] = { 27, 68, 4, 17 }, - [ ATLAS_FONT+117 ] = { 21, 34, 7, 17 }, - [ ATLAS_FONT+118 ] = { 78, 51, 6, 17 }, - [ ATLAS_FONT+119 ] = { 94, 0, 9, 17 }, - [ ATLAS_FONT+120 ] = { 84, 51, 6, 17 }, - [ ATLAS_FONT+121 ] = { 90, 51, 6, 17 }, - [ ATLAS_FONT+122 ] = { 10, 68, 5, 17 }, - [ ATLAS_FONT+123 ] = { 31, 68, 4, 17 }, - [ ATLAS_FONT+124 ] = { 96, 51, 6, 17 }, - [ ATLAS_FONT+125 ] = { 35, 68, 4, 17 }, - [ ATLAS_FONT+126 ] = { 102, 51, 6, 17 }, - [ ATLAS_FONT+127 ] = { 108, 51, 6, 17 }, -}; - blob - 0415e45b105484fcf06fe1d9f4f01875d98cd128 (mode 644) blob + /dev/null --- lib/microui.c +++ /dev/null @@ -1,1208 +0,0 @@ -/* -** Copyright (c) 2024 rxi -** -** Permission is hereby granted, free of charge, to any person obtaining a copy -** of this software and associated documentation files (the "Software"), to -** deal in the Software without restriction, including without limitation the -** rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -** sell copies of the Software, and to permit persons to whom the Software is -** furnished to do so, subject to the following conditions: -** -** The above copyright notice and this permission notice shall be included in -** all copies or substantial portions of the Software. -** -** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -** IN THE SOFTWARE. -*/ - -#include -#include -#include -#include "microui.h" - -#define unused(x) ((void) (x)) - -#define expect(x) do { \ - if (!(x)) { \ - fprintf(stderr, "Fatal error: %s:%d: assertion '%s' failed\n", \ - __FILE__, __LINE__, #x); \ - abort(); \ - } \ - } while (0) - -#define push(stk, val) do { \ - expect((stk).idx < (int) (sizeof((stk).items) / sizeof(*(stk).items))); \ - (stk).items[(stk).idx] = (val); \ - (stk).idx++; /* incremented after incase `val` uses this value */ \ - } while (0) - -#define pop(stk) do { \ - expect((stk).idx > 0); \ - (stk).idx--; \ - } while (0) - - -static mu_Rect unclipped_rect = { 0, 0, 0x1000000, 0x1000000 }; - -static mu_Style default_style = { - /* font | size | padding | spacing | indent */ - NULL, { 68, 10 }, 5, 4, 24, - /* title_height | scrollbar_size | thumb_size */ - 24, 12, 8, - { - { 230, 230, 230, 255 }, /* MU_COLOR_TEXT */ - { 25, 25, 25, 255 }, /* MU_COLOR_BORDER */ - { 50, 50, 50, 255 }, /* MU_COLOR_WINDOWBG */ - { 25, 25, 25, 255 }, /* MU_COLOR_TITLEBG */ - { 240, 240, 240, 255 }, /* MU_COLOR_TITLETEXT */ - { 0, 0, 0, 0 }, /* MU_COLOR_PANELBG */ - { 75, 75, 75, 255 }, /* MU_COLOR_BUTTON */ - { 95, 95, 95, 255 }, /* MU_COLOR_BUTTONHOVER */ - { 115, 115, 115, 255 }, /* MU_COLOR_BUTTONFOCUS */ - { 30, 30, 30, 255 }, /* MU_COLOR_BASE */ - { 35, 35, 35, 255 }, /* MU_COLOR_BASEHOVER */ - { 40, 40, 40, 255 }, /* MU_COLOR_BASEFOCUS */ - { 43, 43, 43, 255 }, /* MU_COLOR_SCROLLBASE */ - { 30, 30, 30, 255 } /* MU_COLOR_SCROLLTHUMB */ - } -}; - - -mu_Vec2 mu_vec2(int x, int y) { - mu_Vec2 res; - res.x = x; res.y = y; - return res; -} - - -mu_Rect mu_rect(int x, int y, int w, int h) { - mu_Rect res; - res.x = x; res.y = y; res.w = w; res.h = h; - return res; -} - - -mu_Color mu_color(int r, int g, int b, int a) { - mu_Color res; - res.r = r; res.g = g; res.b = b; res.a = a; - return res; -} - - -static mu_Rect expand_rect(mu_Rect rect, int n) { - return mu_rect(rect.x - n, rect.y - n, rect.w + n * 2, rect.h + n * 2); -} - - -static mu_Rect intersect_rects(mu_Rect r1, mu_Rect r2) { - int x1 = mu_max(r1.x, r2.x); - int y1 = mu_max(r1.y, r2.y); - int x2 = mu_min(r1.x + r1.w, r2.x + r2.w); - int y2 = mu_min(r1.y + r1.h, r2.y + r2.h); - if (x2 < x1) { x2 = x1; } - if (y2 < y1) { y2 = y1; } - return mu_rect(x1, y1, x2 - x1, y2 - y1); -} - - -static int rect_overlaps_vec2(mu_Rect r, mu_Vec2 p) { - return p.x >= r.x && p.x < r.x + r.w && p.y >= r.y && p.y < r.y + r.h; -} - - -static void draw_frame(mu_Context *ctx, mu_Rect rect, int colorid) { - mu_draw_rect(ctx, rect, ctx->style->colors[colorid]); - if (colorid == MU_COLOR_SCROLLBASE || - colorid == MU_COLOR_SCROLLTHUMB || - colorid == MU_COLOR_TITLEBG) { return; } - /* draw border */ - if (ctx->style->colors[MU_COLOR_BORDER].a) { - mu_draw_box(ctx, expand_rect(rect, 1), ctx->style->colors[MU_COLOR_BORDER]); - } -} - - -void mu_init(mu_Context *ctx) { - memset(ctx, 0, sizeof(*ctx)); - ctx->draw_frame = draw_frame; - ctx->_style = default_style; - ctx->style = &ctx->_style; -} - - -void mu_begin(mu_Context *ctx) { - expect(ctx->text_width && ctx->text_height); - ctx->command_list.idx = 0; - ctx->root_list.idx = 0; - ctx->scroll_target = NULL; - ctx->hover_root = ctx->next_hover_root; - ctx->next_hover_root = NULL; - ctx->mouse_delta.x = ctx->mouse_pos.x - ctx->last_mouse_pos.x; - ctx->mouse_delta.y = ctx->mouse_pos.y - ctx->last_mouse_pos.y; - ctx->frame++; -} - - -static int compare_zindex(const void *a, const void *b) { - return (*(mu_Container**) a)->zindex - (*(mu_Container**) b)->zindex; -} - - -void mu_end(mu_Context *ctx) { - int i, n; - /* check stacks */ - expect(ctx->container_stack.idx == 0); - expect(ctx->clip_stack.idx == 0); - expect(ctx->id_stack.idx == 0); - expect(ctx->layout_stack.idx == 0); - - /* handle scroll input */ - if (ctx->scroll_target) { - ctx->scroll_target->scroll.x += ctx->scroll_delta.x; - ctx->scroll_target->scroll.y += ctx->scroll_delta.y; - } - - /* unset focus if focus id was not touched this frame */ - if (!ctx->updated_focus) { ctx->focus = 0; } - ctx->updated_focus = 0; - - /* bring hover root to front if mouse was pressed */ - if (ctx->mouse_pressed && ctx->next_hover_root && - ctx->next_hover_root->zindex < ctx->last_zindex && - ctx->next_hover_root->zindex >= 0 - ) { - mu_bring_to_front(ctx, ctx->next_hover_root); - } - - /* reset input state */ - ctx->key_pressed = 0; - ctx->input_text[0] = '\0'; - ctx->mouse_pressed = 0; - ctx->scroll_delta = mu_vec2(0, 0); - ctx->last_mouse_pos = ctx->mouse_pos; - - /* sort root containers by zindex */ - n = ctx->root_list.idx; - qsort(ctx->root_list.items, n, sizeof(mu_Container*), compare_zindex); - - /* set root container jump commands */ - for (i = 0; i < n; i++) { - mu_Container *cnt = ctx->root_list.items[i]; - /* if this is the first container then make the first command jump to it. - ** otherwise set the previous container's tail to jump to this one */ - if (i == 0) { - mu_Command *cmd = (mu_Command*) ctx->command_list.items; - cmd->jump.dst = (char*) cnt->head + sizeof(mu_JumpCommand); - } else { - mu_Container *prev = ctx->root_list.items[i - 1]; - prev->tail->jump.dst = (char*) cnt->head + sizeof(mu_JumpCommand); - } - /* make the last container's tail jump to the end of command list */ - if (i == n - 1) { - cnt->tail->jump.dst = ctx->command_list.items + ctx->command_list.idx; - } - } -} - - -void mu_set_focus(mu_Context *ctx, mu_Id id) { - ctx->focus = id; - ctx->updated_focus = 1; -} - - -/* 32bit fnv-1a hash */ -#define HASH_INITIAL 2166136261 - -static void hash(mu_Id *hash, const void *data, int size) { - const unsigned char *p = data; - while (size--) { - *hash = (*hash ^ *p++) * 16777619; - } -} - - -mu_Id mu_get_id(mu_Context *ctx, const void *data, int size) { - int idx = ctx->id_stack.idx; - mu_Id res = (idx > 0) ? ctx->id_stack.items[idx - 1] : HASH_INITIAL; - hash(&res, data, size); - ctx->last_id = res; - return res; -} - - -void mu_push_id(mu_Context *ctx, const void *data, int size) { - push(ctx->id_stack, mu_get_id(ctx, data, size)); -} - - -void mu_pop_id(mu_Context *ctx) { - pop(ctx->id_stack); -} - - -void mu_push_clip_rect(mu_Context *ctx, mu_Rect rect) { - mu_Rect last = mu_get_clip_rect(ctx); - push(ctx->clip_stack, intersect_rects(rect, last)); -} - - -void mu_pop_clip_rect(mu_Context *ctx) { - pop(ctx->clip_stack); -} - - -mu_Rect mu_get_clip_rect(mu_Context *ctx) { - expect(ctx->clip_stack.idx > 0); - return ctx->clip_stack.items[ctx->clip_stack.idx - 1]; -} - - -int mu_check_clip(mu_Context *ctx, mu_Rect r) { - mu_Rect cr = mu_get_clip_rect(ctx); - if (r.x > cr.x + cr.w || r.x + r.w < cr.x || - r.y > cr.y + cr.h || r.y + r.h < cr.y ) { return MU_CLIP_ALL; } - if (r.x >= cr.x && r.x + r.w <= cr.x + cr.w && - r.y >= cr.y && r.y + r.h <= cr.y + cr.h ) { return 0; } - return MU_CLIP_PART; -} - - -static void push_layout(mu_Context *ctx, mu_Rect body, mu_Vec2 scroll) { - mu_Layout layout; - int width = 0; - memset(&layout, 0, sizeof(layout)); - layout.body = mu_rect(body.x - scroll.x, body.y - scroll.y, body.w, body.h); - layout.max = mu_vec2(-0x1000000, -0x1000000); - push(ctx->layout_stack, layout); - mu_layout_row(ctx, 1, &width, 0); -} - - -static mu_Layout* get_layout(mu_Context *ctx) { - return &ctx->layout_stack.items[ctx->layout_stack.idx - 1]; -} - - -static void pop_container(mu_Context *ctx) { - mu_Container *cnt = mu_get_current_container(ctx); - mu_Layout *layout = get_layout(ctx); - cnt->content_size.x = layout->max.x - layout->body.x; - cnt->content_size.y = layout->max.y - layout->body.y; - /* pop container, layout and id */ - pop(ctx->container_stack); - pop(ctx->layout_stack); - mu_pop_id(ctx); -} - - -mu_Container* mu_get_current_container(mu_Context *ctx) { - expect(ctx->container_stack.idx > 0); - return ctx->container_stack.items[ ctx->container_stack.idx - 1 ]; -} - - -static mu_Container* get_container(mu_Context *ctx, mu_Id id, int opt) { - mu_Container *cnt; - /* try to get existing container from pool */ - int idx = mu_pool_get(ctx, ctx->container_pool, MU_CONTAINERPOOL_SIZE, id); - if (idx >= 0) { - if (ctx->containers[idx].open || ~opt & MU_OPT_CLOSED) { - mu_pool_update(ctx, ctx->container_pool, idx); - } - return &ctx->containers[idx]; - } - if (opt & MU_OPT_CLOSED) { return NULL; } - /* container not found in pool: init new container */ - idx = mu_pool_init(ctx, ctx->container_pool, MU_CONTAINERPOOL_SIZE, id); - cnt = &ctx->containers[idx]; - memset(cnt, 0, sizeof(*cnt)); - cnt->open = 1; - mu_bring_to_front(ctx, cnt); - return cnt; -} - - -mu_Container* mu_get_container(mu_Context *ctx, const char *name) { - mu_Id id = mu_get_id(ctx, name, strlen(name)); - return get_container(ctx, id, 0); -} - - -void mu_bring_to_front(mu_Context *ctx, mu_Container *cnt) { - cnt->zindex = ++ctx->last_zindex; -} - - -/*============================================================================ -** pool -**============================================================================*/ - -int mu_pool_init(mu_Context *ctx, mu_PoolItem *items, int len, mu_Id id) { - int i, n = -1, f = ctx->frame; - for (i = 0; i < len; i++) { - if (items[i].last_update < f) { - f = items[i].last_update; - n = i; - } - } - expect(n > -1); - items[n].id = id; - mu_pool_update(ctx, items, n); - return n; -} - - -int mu_pool_get(mu_Context *ctx, mu_PoolItem *items, int len, mu_Id id) { - int i; - unused(ctx); - for (i = 0; i < len; i++) { - if (items[i].id == id) { return i; } - } - return -1; -} - - -void mu_pool_update(mu_Context *ctx, mu_PoolItem *items, int idx) { - items[idx].last_update = ctx->frame; -} - - -/*============================================================================ -** input handlers -**============================================================================*/ - -void mu_input_mousemove(mu_Context *ctx, int x, int y) { - ctx->mouse_pos = mu_vec2(x, y); -} - - -void mu_input_mousedown(mu_Context *ctx, int x, int y, int btn) { - mu_input_mousemove(ctx, x, y); - ctx->mouse_down |= btn; - ctx->mouse_pressed |= btn; -} - - -void mu_input_mouseup(mu_Context *ctx, int x, int y, int btn) { - mu_input_mousemove(ctx, x, y); - ctx->mouse_down &= ~btn; -} - - -void mu_input_scroll(mu_Context *ctx, int x, int y) { - ctx->scroll_delta.x += x; - ctx->scroll_delta.y += y; -} - - -void mu_input_keydown(mu_Context *ctx, int key) { - ctx->key_pressed |= key; - ctx->key_down |= key; -} - - -void mu_input_keyup(mu_Context *ctx, int key) { - ctx->key_down &= ~key; -} - - -void mu_input_text(mu_Context *ctx, const char *text) { - int len = strlen(ctx->input_text); - int size = strlen(text) + 1; - expect(len + size <= (int) sizeof(ctx->input_text)); - memcpy(ctx->input_text + len, text, size); -} - - -/*============================================================================ -** commandlist -**============================================================================*/ - -mu_Command* mu_push_command(mu_Context *ctx, int type, int size) { - mu_Command *cmd = (mu_Command*) (ctx->command_list.items + ctx->command_list.idx); - expect(ctx->command_list.idx + size < MU_COMMANDLIST_SIZE); - cmd->base.type = type; - cmd->base.size = size; - ctx->command_list.idx += size; - return cmd; -} - - -int mu_next_command(mu_Context *ctx, mu_Command **cmd) { - if (*cmd) { - *cmd = (mu_Command*) (((char*) *cmd) + (*cmd)->base.size); - } else { - *cmd = (mu_Command*) ctx->command_list.items; - } - while ((char*) *cmd != ctx->command_list.items + ctx->command_list.idx) { - if ((*cmd)->type != MU_COMMAND_JUMP) { return 1; } - *cmd = (*cmd)->jump.dst; - } - return 0; -} - - -static mu_Command* push_jump(mu_Context *ctx, mu_Command *dst) { - mu_Command *cmd; - cmd = mu_push_command(ctx, MU_COMMAND_JUMP, sizeof(mu_JumpCommand)); - cmd->jump.dst = dst; - return cmd; -} - - -void mu_set_clip(mu_Context *ctx, mu_Rect rect) { - mu_Command *cmd; - cmd = mu_push_command(ctx, MU_COMMAND_CLIP, sizeof(mu_ClipCommand)); - cmd->clip.rect = rect; -} - - -void mu_draw_rect(mu_Context *ctx, mu_Rect rect, mu_Color color) { - mu_Command *cmd; - rect = intersect_rects(rect, mu_get_clip_rect(ctx)); - if (rect.w > 0 && rect.h > 0) { - cmd = mu_push_command(ctx, MU_COMMAND_RECT, sizeof(mu_RectCommand)); - cmd->rect.rect = rect; - cmd->rect.color = color; - } -} - - -void mu_draw_box(mu_Context *ctx, mu_Rect rect, mu_Color color) { - mu_draw_rect(ctx, mu_rect(rect.x + 1, rect.y, rect.w - 2, 1), color); - mu_draw_rect(ctx, mu_rect(rect.x + 1, rect.y + rect.h - 1, rect.w - 2, 1), color); - mu_draw_rect(ctx, mu_rect(rect.x, rect.y, 1, rect.h), color); - mu_draw_rect(ctx, mu_rect(rect.x + rect.w - 1, rect.y, 1, rect.h), color); -} - - -void mu_draw_text(mu_Context *ctx, mu_Font font, const char *str, int len, - mu_Vec2 pos, mu_Color color) -{ - mu_Command *cmd; - mu_Rect rect = mu_rect( - pos.x, pos.y, ctx->text_width(font, str, len), ctx->text_height(font)); - int clipped = mu_check_clip(ctx, rect); - if (clipped == MU_CLIP_ALL ) { return; } - if (clipped == MU_CLIP_PART) { mu_set_clip(ctx, mu_get_clip_rect(ctx)); } - /* add command */ - if (len < 0) { len = strlen(str); } - cmd = mu_push_command(ctx, MU_COMMAND_TEXT, sizeof(mu_TextCommand) + len); - memcpy(cmd->text.str, str, len); - cmd->text.str[len] = '\0'; - cmd->text.pos = pos; - cmd->text.color = color; - cmd->text.font = font; - /* reset clipping if it was set */ - if (clipped) { mu_set_clip(ctx, unclipped_rect); } -} - - -void mu_draw_icon(mu_Context *ctx, int id, mu_Rect rect, mu_Color color) { - mu_Command *cmd; - /* do clip command if the rect isn't fully contained within the cliprect */ - int clipped = mu_check_clip(ctx, rect); - if (clipped == MU_CLIP_ALL ) { return; } - if (clipped == MU_CLIP_PART) { mu_set_clip(ctx, mu_get_clip_rect(ctx)); } - /* do icon command */ - cmd = mu_push_command(ctx, MU_COMMAND_ICON, sizeof(mu_IconCommand)); - cmd->icon.id = id; - cmd->icon.rect = rect; - cmd->icon.color = color; - /* reset clipping if it was set */ - if (clipped) { mu_set_clip(ctx, unclipped_rect); } -} - - -/*============================================================================ -** layout -**============================================================================*/ - -enum { RELATIVE = 1, ABSOLUTE = 2 }; - - -void mu_layout_begin_column(mu_Context *ctx) { - push_layout(ctx, mu_layout_next(ctx), mu_vec2(0, 0)); -} - - -void mu_layout_end_column(mu_Context *ctx) { - mu_Layout *a, *b; - b = get_layout(ctx); - pop(ctx->layout_stack); - /* inherit position/next_row/max from child layout if they are greater */ - a = get_layout(ctx); - a->position.x = mu_max(a->position.x, b->position.x + b->body.x - a->body.x); - a->next_row = mu_max(a->next_row, b->next_row + b->body.y - a->body.y); - a->max.x = mu_max(a->max.x, b->max.x); - a->max.y = mu_max(a->max.y, b->max.y); -} - - -void mu_layout_row(mu_Context *ctx, int items, const int *widths, int height) { - mu_Layout *layout = get_layout(ctx); - if (widths) { - expect(items <= MU_MAX_WIDTHS); - memcpy(layout->widths, widths, items * sizeof(widths[0])); - } - layout->items = items; - layout->position = mu_vec2(layout->indent, layout->next_row); - layout->size.y = height; - layout->item_index = 0; -} - - -void mu_layout_width(mu_Context *ctx, int width) { - get_layout(ctx)->size.x = width; -} - - -void mu_layout_height(mu_Context *ctx, int height) { - get_layout(ctx)->size.y = height; -} - - -void mu_layout_set_next(mu_Context *ctx, mu_Rect r, int relative) { - mu_Layout *layout = get_layout(ctx); - layout->next = r; - layout->next_type = relative ? RELATIVE : ABSOLUTE; -} - - -mu_Rect mu_layout_next(mu_Context *ctx) { - mu_Layout *layout = get_layout(ctx); - mu_Style *style = ctx->style; - mu_Rect res; - - if (layout->next_type) { - /* handle rect set by `mu_layout_set_next` */ - int type = layout->next_type; - layout->next_type = 0; - res = layout->next; - if (type == ABSOLUTE) { return (ctx->last_rect = res); } - - } else { - /* handle next row */ - if (layout->item_index == layout->items) { - mu_layout_row(ctx, layout->items, NULL, layout->size.y); - } - - /* position */ - res.x = layout->position.x; - res.y = layout->position.y; - - /* size */ - res.w = layout->items > 0 ? layout->widths[layout->item_index] : layout->size.x; - res.h = layout->size.y; - if (res.w == 0) { res.w = style->size.x + style->padding * 2; } - if (res.h == 0) { res.h = style->size.y + style->padding * 2; } - if (res.w < 0) { res.w += layout->body.w - res.x + 1; } - if (res.h < 0) { res.h += layout->body.h - res.y + 1; } - - layout->item_index++; - } - - /* update position */ - layout->position.x += res.w + style->spacing; - layout->next_row = mu_max(layout->next_row, res.y + res.h + style->spacing); - - /* apply body offset */ - res.x += layout->body.x; - res.y += layout->body.y; - - /* update max position */ - layout->max.x = mu_max(layout->max.x, res.x + res.w); - layout->max.y = mu_max(layout->max.y, res.y + res.h); - - return (ctx->last_rect = res); -} - - -/*============================================================================ -** controls -**============================================================================*/ - -static int in_hover_root(mu_Context *ctx) { - int i = ctx->container_stack.idx; - while (i--) { - if (ctx->container_stack.items[i] == ctx->hover_root) { return 1; } - /* only root containers have their `head` field set; stop searching if we've - ** reached the current root container */ - if (ctx->container_stack.items[i]->head) { break; } - } - return 0; -} - - -void mu_draw_control_frame(mu_Context *ctx, mu_Id id, mu_Rect rect, - int colorid, int opt) -{ - if (opt & MU_OPT_NOFRAME) { return; } - colorid += (ctx->focus == id) ? 2 : (ctx->hover == id) ? 1 : 0; - ctx->draw_frame(ctx, rect, colorid); -} - - -void mu_draw_control_text(mu_Context *ctx, const char *str, mu_Rect rect, - int colorid, int opt) -{ - mu_Vec2 pos; - mu_Font font = ctx->style->font; - int tw = ctx->text_width(font, str, -1); - mu_push_clip_rect(ctx, rect); - pos.y = rect.y + (rect.h - ctx->text_height(font)) / 2; - if (opt & MU_OPT_ALIGNCENTER) { - pos.x = rect.x + (rect.w - tw) / 2; - } else if (opt & MU_OPT_ALIGNRIGHT) { - pos.x = rect.x + rect.w - tw - ctx->style->padding; - } else { - pos.x = rect.x + ctx->style->padding; - } - mu_draw_text(ctx, font, str, -1, pos, ctx->style->colors[colorid]); - mu_pop_clip_rect(ctx); -} - - -int mu_mouse_over(mu_Context *ctx, mu_Rect rect) { - return rect_overlaps_vec2(rect, ctx->mouse_pos) && - rect_overlaps_vec2(mu_get_clip_rect(ctx), ctx->mouse_pos) && - in_hover_root(ctx); -} - - -void mu_update_control(mu_Context *ctx, mu_Id id, mu_Rect rect, int opt) { - int mouseover = mu_mouse_over(ctx, rect); - - if (ctx->focus == id) { ctx->updated_focus = 1; } - if (opt & MU_OPT_NOINTERACT) { return; } - if (mouseover && !ctx->mouse_down) { ctx->hover = id; } - - if (ctx->focus == id) { - if (ctx->mouse_pressed && !mouseover) { mu_set_focus(ctx, 0); } - if (!ctx->mouse_down && ~opt & MU_OPT_HOLDFOCUS) { mu_set_focus(ctx, 0); } - } - - if (ctx->hover == id) { - if (ctx->mouse_pressed) { - mu_set_focus(ctx, id); - } else if (!mouseover) { - ctx->hover = 0; - } - } -} - - -void mu_text(mu_Context *ctx, const char *text) { - const char *start, *end, *p = text; - int width = -1; - mu_Font font = ctx->style->font; - mu_Color color = ctx->style->colors[MU_COLOR_TEXT]; - mu_layout_begin_column(ctx); - mu_layout_row(ctx, 1, &width, ctx->text_height(font)); - do { - mu_Rect r = mu_layout_next(ctx); - int w = 0; - start = end = p; - do { - const char* word = p; - while (*p && *p != ' ' && *p != '\n') { p++; } - w += ctx->text_width(font, word, p - word); - if (w > r.w && end != start) { break; } - w += ctx->text_width(font, p, 1); - end = p++; - } while (*end && *end != '\n'); - mu_draw_text(ctx, font, start, end - start, mu_vec2(r.x, r.y), color); - p = end + 1; - } while (*end); - mu_layout_end_column(ctx); -} - - -void mu_label(mu_Context *ctx, const char *text) { - mu_draw_control_text(ctx, text, mu_layout_next(ctx), MU_COLOR_TEXT, 0); -} - - -int mu_button_ex(mu_Context *ctx, const char *label, int icon, int opt) { - int res = 0; - mu_Id id = label ? mu_get_id(ctx, label, strlen(label)) - : mu_get_id(ctx, &icon, sizeof(icon)); - mu_Rect r = mu_layout_next(ctx); - mu_update_control(ctx, id, r, opt); - /* handle click */ - if (ctx->mouse_pressed == MU_MOUSE_LEFT && ctx->focus == id) { - res |= MU_RES_SUBMIT; - } - /* draw */ - mu_draw_control_frame(ctx, id, r, MU_COLOR_BUTTON, opt); - if (label) { mu_draw_control_text(ctx, label, r, MU_COLOR_TEXT, opt); } - if (icon) { mu_draw_icon(ctx, icon, r, ctx->style->colors[MU_COLOR_TEXT]); } - return res; -} - - -int mu_checkbox(mu_Context *ctx, const char *label, int *state) { - int res = 0; - mu_Id id = mu_get_id(ctx, &state, sizeof(state)); - mu_Rect r = mu_layout_next(ctx); - mu_Rect box = mu_rect(r.x, r.y, r.h, r.h); - mu_update_control(ctx, id, r, 0); - /* handle click */ - if (ctx->mouse_pressed == MU_MOUSE_LEFT && ctx->focus == id) { - res |= MU_RES_CHANGE; - *state = !*state; - } - /* draw */ - mu_draw_control_frame(ctx, id, box, MU_COLOR_BASE, 0); - if (*state) { - mu_draw_icon(ctx, MU_ICON_CHECK, box, ctx->style->colors[MU_COLOR_TEXT]); - } - r = mu_rect(r.x + box.w, r.y, r.w - box.w, r.h); - mu_draw_control_text(ctx, label, r, MU_COLOR_TEXT, 0); - return res; -} - - -int mu_textbox_raw(mu_Context *ctx, char *buf, int bufsz, mu_Id id, mu_Rect r, - int opt) -{ - int res = 0; - mu_update_control(ctx, id, r, opt | MU_OPT_HOLDFOCUS); - - if (ctx->focus == id) { - /* handle text input */ - int len = strlen(buf); - int n = mu_min(bufsz - len - 1, (int) strlen(ctx->input_text)); - if (n > 0) { - memcpy(buf + len, ctx->input_text, n); - len += n; - buf[len] = '\0'; - res |= MU_RES_CHANGE; - } - /* handle backspace */ - if (ctx->key_pressed & MU_KEY_BACKSPACE && len > 0) { - /* skip utf-8 continuation bytes */ - while ((buf[--len] & 0xc0) == 0x80 && len > 0); - buf[len] = '\0'; - res |= MU_RES_CHANGE; - } - /* handle return */ - if (ctx->key_pressed & MU_KEY_RETURN) { - mu_set_focus(ctx, 0); - res |= MU_RES_SUBMIT; - } - } - - /* draw */ - mu_draw_control_frame(ctx, id, r, MU_COLOR_BASE, opt); - if (ctx->focus == id) { - mu_Color color = ctx->style->colors[MU_COLOR_TEXT]; - mu_Font font = ctx->style->font; - int textw = ctx->text_width(font, buf, -1); - int texth = ctx->text_height(font); - int ofx = r.w - ctx->style->padding - textw - 1; - int textx = r.x + mu_min(ofx, ctx->style->padding); - int texty = r.y + (r.h - texth) / 2; - mu_push_clip_rect(ctx, r); - mu_draw_text(ctx, font, buf, -1, mu_vec2(textx, texty), color); - mu_draw_rect(ctx, mu_rect(textx + textw, texty, 1, texth), color); - mu_pop_clip_rect(ctx); - } else { - mu_draw_control_text(ctx, buf, r, MU_COLOR_TEXT, opt); - } - - return res; -} - - -static int number_textbox(mu_Context *ctx, mu_Real *value, mu_Rect r, mu_Id id) { - if (ctx->mouse_pressed == MU_MOUSE_LEFT && ctx->key_down & MU_KEY_SHIFT && - ctx->hover == id - ) { - ctx->number_edit = id; - sprintf(ctx->number_edit_buf, MU_REAL_FMT, *value); - } - if (ctx->number_edit == id) { - int res = mu_textbox_raw( - ctx, ctx->number_edit_buf, sizeof(ctx->number_edit_buf), id, r, 0); - if (res & MU_RES_SUBMIT || ctx->focus != id) { - *value = strtod(ctx->number_edit_buf, NULL); - ctx->number_edit = 0; - } else { - return 1; - } - } - return 0; -} - - -int mu_textbox_ex(mu_Context *ctx, char *buf, int bufsz, int opt) { - mu_Id id = mu_get_id(ctx, &buf, sizeof(buf)); - mu_Rect r = mu_layout_next(ctx); - return mu_textbox_raw(ctx, buf, bufsz, id, r, opt); -} - - -int mu_slider_ex(mu_Context *ctx, mu_Real *value, mu_Real low, mu_Real high, - mu_Real step, const char *fmt, int opt) -{ - char buf[MU_MAX_FMT + 1]; - mu_Rect thumb; - int x, w, res = 0; - mu_Real last = *value, v = last; - mu_Id id = mu_get_id(ctx, &value, sizeof(value)); - mu_Rect base = mu_layout_next(ctx); - - /* handle text input mode */ - if (number_textbox(ctx, &v, base, id)) { return res; } - - /* handle normal mode */ - mu_update_control(ctx, id, base, opt); - - /* handle input */ - if (ctx->focus == id && - (ctx->mouse_down | ctx->mouse_pressed) == MU_MOUSE_LEFT) - { - v = low + (ctx->mouse_pos.x - base.x) * (high - low) / base.w; - if (step) { v = ((long long)((v + step / 2) / step)) * step; } - } - /* clamp and store value, update res */ - *value = v = mu_clamp(v, low, high); - if (last != v) { res |= MU_RES_CHANGE; } - - /* draw base */ - mu_draw_control_frame(ctx, id, base, MU_COLOR_BASE, opt); - /* draw thumb */ - w = ctx->style->thumb_size; - x = (v - low) * (base.w - w) / (high - low); - thumb = mu_rect(base.x + x, base.y, w, base.h); - mu_draw_control_frame(ctx, id, thumb, MU_COLOR_BUTTON, opt); - /* draw text */ - sprintf(buf, fmt, v); - mu_draw_control_text(ctx, buf, base, MU_COLOR_TEXT, opt); - - return res; -} - - -int mu_number_ex(mu_Context *ctx, mu_Real *value, mu_Real step, - const char *fmt, int opt) -{ - char buf[MU_MAX_FMT + 1]; - int res = 0; - mu_Id id = mu_get_id(ctx, &value, sizeof(value)); - mu_Rect base = mu_layout_next(ctx); - mu_Real last = *value; - - /* handle text input mode */ - if (number_textbox(ctx, value, base, id)) { return res; } - - /* handle normal mode */ - mu_update_control(ctx, id, base, opt); - - /* handle input */ - if (ctx->focus == id && ctx->mouse_down == MU_MOUSE_LEFT) { - *value += ctx->mouse_delta.x * step; - } - /* set flag if value changed */ - if (*value != last) { res |= MU_RES_CHANGE; } - - /* draw base */ - mu_draw_control_frame(ctx, id, base, MU_COLOR_BASE, opt); - /* draw text */ - sprintf(buf, fmt, *value); - mu_draw_control_text(ctx, buf, base, MU_COLOR_TEXT, opt); - - return res; -} - - -static int header(mu_Context *ctx, const char *label, int istreenode, int opt) { - mu_Rect r; - int active, expanded; - mu_Id id = mu_get_id(ctx, label, strlen(label)); - int idx = mu_pool_get(ctx, ctx->treenode_pool, MU_TREENODEPOOL_SIZE, id); - int width = -1; - mu_layout_row(ctx, 1, &width, 0); - - active = (idx >= 0); - expanded = (opt & MU_OPT_EXPANDED) ? !active : active; - r = mu_layout_next(ctx); - mu_update_control(ctx, id, r, 0); - - /* handle click */ - active ^= (ctx->mouse_pressed == MU_MOUSE_LEFT && ctx->focus == id); - - /* update pool ref */ - if (idx >= 0) { - if (active) { mu_pool_update(ctx, ctx->treenode_pool, idx); } - else { memset(&ctx->treenode_pool[idx], 0, sizeof(mu_PoolItem)); } - } else if (active) { - mu_pool_init(ctx, ctx->treenode_pool, MU_TREENODEPOOL_SIZE, id); - } - - /* draw */ - if (istreenode) { - if (ctx->hover == id) { ctx->draw_frame(ctx, r, MU_COLOR_BUTTONHOVER); } - } else { - mu_draw_control_frame(ctx, id, r, MU_COLOR_BUTTON, 0); - } - mu_draw_icon( - ctx, expanded ? MU_ICON_EXPANDED : MU_ICON_COLLAPSED, - mu_rect(r.x, r.y, r.h, r.h), ctx->style->colors[MU_COLOR_TEXT]); - r.x += r.h - ctx->style->padding; - r.w -= r.h - ctx->style->padding; - mu_draw_control_text(ctx, label, r, MU_COLOR_TEXT, 0); - - return expanded ? MU_RES_ACTIVE : 0; -} - - -int mu_header_ex(mu_Context *ctx, const char *label, int opt) { - return header(ctx, label, 0, opt); -} - - -int mu_begin_treenode_ex(mu_Context *ctx, const char *label, int opt) { - int res = header(ctx, label, 1, opt); - if (res & MU_RES_ACTIVE) { - get_layout(ctx)->indent += ctx->style->indent; - push(ctx->id_stack, ctx->last_id); - } - return res; -} - - -void mu_end_treenode(mu_Context *ctx) { - get_layout(ctx)->indent -= ctx->style->indent; - mu_pop_id(ctx); -} - - -#define scrollbar(ctx, cnt, b, cs, x, y, w, h) \ - do { \ - /* only add scrollbar if content size is larger than body */ \ - int maxscroll = cs.y - b->h; \ - \ - if (maxscroll > 0 && b->h > 0) { \ - mu_Rect base, thumb; \ - mu_Id id = mu_get_id(ctx, "!scrollbar" #y, 11); \ - \ - /* get sizing / positioning */ \ - base = *b; \ - base.x = b->x + b->w; \ - base.w = ctx->style->scrollbar_size; \ - \ - /* handle input */ \ - mu_update_control(ctx, id, base, 0); \ - if (ctx->focus == id && ctx->mouse_down == MU_MOUSE_LEFT) { \ - cnt->scroll.y += ctx->mouse_delta.y * cs.y / base.h; \ - } \ - /* clamp scroll to limits */ \ - cnt->scroll.y = mu_clamp(cnt->scroll.y, 0, maxscroll); \ - \ - /* draw base and thumb */ \ - ctx->draw_frame(ctx, base, MU_COLOR_SCROLLBASE); \ - thumb = base; \ - thumb.h = mu_max(ctx->style->thumb_size, base.h * b->h / cs.y); \ - thumb.y += cnt->scroll.y * (base.h - thumb.h) / maxscroll; \ - ctx->draw_frame(ctx, thumb, MU_COLOR_SCROLLTHUMB); \ - \ - /* set this as the scroll_target (will get scrolled on mousewheel) */ \ - /* if the mouse is over it */ \ - if (mu_mouse_over(ctx, *b)) { ctx->scroll_target = cnt; } \ - } else { \ - cnt->scroll.y = 0; \ - } \ - } while (0) - - -static void scrollbars(mu_Context *ctx, mu_Container *cnt, mu_Rect *body) { - int sz = ctx->style->scrollbar_size; - mu_Vec2 cs = cnt->content_size; - cs.x += ctx->style->padding * 2; - cs.y += ctx->style->padding * 2; - mu_push_clip_rect(ctx, *body); - /* resize body to make room for scrollbars */ - if (cs.y > cnt->body.h) { body->w -= sz; } - if (cs.x > cnt->body.w) { body->h -= sz; } - /* to create a horizontal or vertical scrollbar almost-identical code is - ** used; only the references to `x|y` `w|h` need to be switched */ - scrollbar(ctx, cnt, body, cs, x, y, w, h); - scrollbar(ctx, cnt, body, cs, y, x, h, w); - mu_pop_clip_rect(ctx); -} - - -static void push_container_body( - mu_Context *ctx, mu_Container *cnt, mu_Rect body, int opt -) { - if (~opt & MU_OPT_NOSCROLL) { scrollbars(ctx, cnt, &body); } - push_layout(ctx, expand_rect(body, -ctx->style->padding), cnt->scroll); - cnt->body = body; -} - - -static void begin_root_container(mu_Context *ctx, mu_Container *cnt) { - push(ctx->container_stack, cnt); - /* push container to roots list and push head command */ - push(ctx->root_list, cnt); - cnt->head = push_jump(ctx, NULL); - /* set as hover root if the mouse is overlapping this container and it has a - ** higher zindex than the current hover root */ - if (rect_overlaps_vec2(cnt->rect, ctx->mouse_pos) && - (!ctx->next_hover_root || cnt->zindex > ctx->next_hover_root->zindex) - ) { - ctx->next_hover_root = cnt; - } - /* clipping is reset here in case a root-container is made within - ** another root-containers's begin/end block; this prevents the inner - ** root-container being clipped to the outer */ - push(ctx->clip_stack, unclipped_rect); -} - - -static void end_root_container(mu_Context *ctx) { - /* push tail 'goto' jump command and set head 'skip' command. the final steps - ** on initing these are done in mu_end() */ - mu_Container *cnt = mu_get_current_container(ctx); - cnt->tail = push_jump(ctx, NULL); - cnt->head->jump.dst = ctx->command_list.items + ctx->command_list.idx; - /* pop base clip rect and container */ - mu_pop_clip_rect(ctx); - pop_container(ctx); -} - - -int mu_begin_window_ex(mu_Context *ctx, const char *title, mu_Rect rect, int opt) { - mu_Rect body; - mu_Id id = mu_get_id(ctx, title, strlen(title)); - mu_Container *cnt = get_container(ctx, id, opt); - if (!cnt || !cnt->open) { return 0; } - push(ctx->id_stack, id); - - if (cnt->rect.w == 0) { cnt->rect = rect; } - begin_root_container(ctx, cnt); - rect = body = cnt->rect; - - /* draw frame */ - if (~opt & MU_OPT_NOFRAME) { - ctx->draw_frame(ctx, rect, MU_COLOR_WINDOWBG); - } - - /* do title bar */ - if (~opt & MU_OPT_NOTITLE) { - mu_Rect tr = rect; - tr.h = ctx->style->title_height; - ctx->draw_frame(ctx, tr, MU_COLOR_TITLEBG); - - /* do title text */ - if (~opt & MU_OPT_NOTITLE) { - mu_Id id = mu_get_id(ctx, "!title", 6); - mu_update_control(ctx, id, tr, opt); - mu_draw_control_text(ctx, title, tr, MU_COLOR_TITLETEXT, opt); - if (id == ctx->focus && ctx->mouse_down == MU_MOUSE_LEFT) { - cnt->rect.x += ctx->mouse_delta.x; - cnt->rect.y += ctx->mouse_delta.y; - } - body.y += tr.h; - body.h -= tr.h; - } - - /* do `close` button */ - if (~opt & MU_OPT_NOCLOSE) { - mu_Id id = mu_get_id(ctx, "!close", 6); - mu_Rect r = mu_rect(tr.x + tr.w - tr.h, tr.y, tr.h, tr.h); - tr.w -= r.w; - mu_draw_icon(ctx, MU_ICON_CLOSE, r, ctx->style->colors[MU_COLOR_TITLETEXT]); - mu_update_control(ctx, id, r, opt); - if (ctx->mouse_pressed == MU_MOUSE_LEFT && id == ctx->focus) { - cnt->open = 0; - } - } - } - - push_container_body(ctx, cnt, body, opt); - - /* do `resize` handle */ - if (~opt & MU_OPT_NORESIZE) { - int sz = ctx->style->title_height; - mu_Id id = mu_get_id(ctx, "!resize", 7); - mu_Rect r = mu_rect(rect.x + rect.w - sz, rect.y + rect.h - sz, sz, sz); - mu_update_control(ctx, id, r, opt); - if (id == ctx->focus && ctx->mouse_down == MU_MOUSE_LEFT) { - cnt->rect.w = mu_max(96, cnt->rect.w + ctx->mouse_delta.x); - cnt->rect.h = mu_max(64, cnt->rect.h + ctx->mouse_delta.y); - } - } - - /* resize to content size */ - if (opt & MU_OPT_AUTOSIZE) { - mu_Rect r = get_layout(ctx)->body; - cnt->rect.w = cnt->content_size.x + (cnt->rect.w - r.w); - cnt->rect.h = cnt->content_size.y + (cnt->rect.h - r.h); - } - - /* close if this is a popup window and elsewhere was clicked */ - if (opt & MU_OPT_POPUP && ctx->mouse_pressed && ctx->hover_root != cnt) { - cnt->open = 0; - } - - mu_push_clip_rect(ctx, cnt->body); - return MU_RES_ACTIVE; -} - - -void mu_end_window(mu_Context *ctx) { - mu_pop_clip_rect(ctx); - end_root_container(ctx); -} - - -void mu_open_popup(mu_Context *ctx, const char *name) { - mu_Container *cnt = mu_get_container(ctx, name); - /* set as hover root so popup isn't closed in begin_window_ex() */ - ctx->hover_root = ctx->next_hover_root = cnt; - /* position at mouse cursor, open and bring-to-front */ - cnt->rect = mu_rect(ctx->mouse_pos.x, ctx->mouse_pos.y, 1, 1); - cnt->open = 1; - mu_bring_to_front(ctx, cnt); -} - - -int mu_begin_popup(mu_Context *ctx, const char *name) { - int opt = MU_OPT_POPUP | MU_OPT_AUTOSIZE | MU_OPT_NORESIZE | - MU_OPT_NOSCROLL | MU_OPT_NOTITLE | MU_OPT_CLOSED; - return mu_begin_window_ex(ctx, name, mu_rect(0, 0, 0, 0), opt); -} - - -void mu_end_popup(mu_Context *ctx) { - mu_end_window(ctx); -} - - -void mu_begin_panel_ex(mu_Context *ctx, const char *name, int opt) { - mu_Container *cnt; - mu_push_id(ctx, name, strlen(name)); - cnt = get_container(ctx, ctx->last_id, opt); - cnt->rect = mu_layout_next(ctx); - if (~opt & MU_OPT_NOFRAME) { - ctx->draw_frame(ctx, cnt->rect, MU_COLOR_PANELBG); - } - push(ctx->container_stack, cnt); - push_container_body(ctx, cnt, cnt->rect, opt); - mu_push_clip_rect(ctx, cnt->body); -} - - -void mu_end_panel(mu_Context *ctx) { - mu_pop_clip_rect(ctx); - pop_container(ctx); -} blob - 269087cb4d884cf48fdd9510e2446c0282b6248b (mode 644) blob + /dev/null --- lib/microui.h +++ /dev/null @@ -1,296 +0,0 @@ -/* -** Copyright (c) 2024 rxi -** -** This library is free software; you can redistribute it and/or modify it -** under the terms of the MIT license. See `microui.c` for details. -*/ - -#ifndef MICROUI_H -#define MICROUI_H - -#define MU_VERSION "2.02" - -#define MU_COMMANDLIST_SIZE (256 * 1024) -#define MU_ROOTLIST_SIZE 32 -#define MU_CONTAINERSTACK_SIZE 32 -#define MU_CLIPSTACK_SIZE 32 -#define MU_IDSTACK_SIZE 32 -#define MU_LAYOUTSTACK_SIZE 16 -#define MU_CONTAINERPOOL_SIZE 48 -#define MU_TREENODEPOOL_SIZE 48 -#define MU_MAX_WIDTHS 16 -#define MU_REAL float -#define MU_REAL_FMT "%.3g" -#define MU_SLIDER_FMT "%.2f" -#define MU_MAX_FMT 127 - -#define mu_stack(T, n) struct { int idx; T items[n]; } -#define mu_min(a, b) ((a) < (b) ? (a) : (b)) -#define mu_max(a, b) ((a) > (b) ? (a) : (b)) -#define mu_clamp(x, a, b) mu_min(b, mu_max(a, x)) - -enum { - MU_CLIP_PART = 1, - MU_CLIP_ALL -}; - -enum { - MU_COMMAND_JUMP = 1, - MU_COMMAND_CLIP, - MU_COMMAND_RECT, - MU_COMMAND_TEXT, - MU_COMMAND_ICON, - MU_COMMAND_MAX -}; - -enum { - MU_COLOR_TEXT, - MU_COLOR_BORDER, - MU_COLOR_WINDOWBG, - MU_COLOR_TITLEBG, - MU_COLOR_TITLETEXT, - MU_COLOR_PANELBG, - MU_COLOR_BUTTON, - MU_COLOR_BUTTONHOVER, - MU_COLOR_BUTTONFOCUS, - MU_COLOR_BASE, - MU_COLOR_BASEHOVER, - MU_COLOR_BASEFOCUS, - MU_COLOR_SCROLLBASE, - MU_COLOR_SCROLLTHUMB, - MU_COLOR_MAX -}; - -enum { - MU_ICON_CLOSE = 1, - MU_ICON_CHECK, - MU_ICON_COLLAPSED, - MU_ICON_EXPANDED, - MU_ICON_MAX -}; - -enum { - MU_RES_ACTIVE = (1 << 0), - MU_RES_SUBMIT = (1 << 1), - MU_RES_CHANGE = (1 << 2) -}; - -enum { - MU_OPT_ALIGNCENTER = (1 << 0), - MU_OPT_ALIGNRIGHT = (1 << 1), - MU_OPT_NOINTERACT = (1 << 2), - MU_OPT_NOFRAME = (1 << 3), - MU_OPT_NORESIZE = (1 << 4), - MU_OPT_NOSCROLL = (1 << 5), - MU_OPT_NOCLOSE = (1 << 6), - MU_OPT_NOTITLE = (1 << 7), - MU_OPT_HOLDFOCUS = (1 << 8), - MU_OPT_AUTOSIZE = (1 << 9), - MU_OPT_POPUP = (1 << 10), - MU_OPT_CLOSED = (1 << 11), - MU_OPT_EXPANDED = (1 << 12) -}; - -enum { - MU_MOUSE_LEFT = (1 << 0), - MU_MOUSE_RIGHT = (1 << 1), - MU_MOUSE_MIDDLE = (1 << 2) -}; - -enum { - MU_KEY_SHIFT = (1 << 0), - MU_KEY_CTRL = (1 << 1), - MU_KEY_ALT = (1 << 2), - MU_KEY_BACKSPACE = (1 << 3), - MU_KEY_RETURN = (1 << 4) -}; - - -typedef struct mu_Context mu_Context; -typedef unsigned mu_Id; -typedef MU_REAL mu_Real; -typedef void* mu_Font; - -typedef struct { int x, y; } mu_Vec2; -typedef struct { int x, y, w, h; } mu_Rect; -typedef struct { unsigned char r, g, b, a; } mu_Color; -typedef struct { mu_Id id; int last_update; } mu_PoolItem; - -typedef struct { int type, size; } mu_BaseCommand; -typedef struct { mu_BaseCommand base; void *dst; } mu_JumpCommand; -typedef struct { mu_BaseCommand base; mu_Rect rect; } mu_ClipCommand; -typedef struct { mu_BaseCommand base; mu_Rect rect; mu_Color color; } mu_RectCommand; -typedef struct { mu_BaseCommand base; mu_Font font; mu_Vec2 pos; mu_Color color; char str[1]; } mu_TextCommand; -typedef struct { mu_BaseCommand base; mu_Rect rect; int id; mu_Color color; } mu_IconCommand; - -typedef union { - int type; - mu_BaseCommand base; - mu_JumpCommand jump; - mu_ClipCommand clip; - mu_RectCommand rect; - mu_TextCommand text; - mu_IconCommand icon; -} mu_Command; - -typedef struct { - mu_Rect body; - mu_Rect next; - mu_Vec2 position; - mu_Vec2 size; - mu_Vec2 max; - int widths[MU_MAX_WIDTHS]; - int items; - int item_index; - int next_row; - int next_type; - int indent; -} mu_Layout; - -typedef struct { - mu_Command *head, *tail; - mu_Rect rect; - mu_Rect body; - mu_Vec2 content_size; - mu_Vec2 scroll; - int zindex; - int open; -} mu_Container; - -typedef struct { - mu_Font font; - mu_Vec2 size; - int padding; - int spacing; - int indent; - int title_height; - int scrollbar_size; - int thumb_size; - mu_Color colors[MU_COLOR_MAX]; -} mu_Style; - -struct mu_Context { - /* callbacks */ - int (*text_width)(mu_Font font, const char *str, int len); - int (*text_height)(mu_Font font); - void (*draw_frame)(mu_Context *ctx, mu_Rect rect, int colorid); - /* core state */ - mu_Style _style; - mu_Style *style; - mu_Id hover; - mu_Id focus; - mu_Id last_id; - mu_Rect last_rect; - int last_zindex; - int updated_focus; - int frame; - mu_Container *hover_root; - mu_Container *next_hover_root; - mu_Container *scroll_target; - char number_edit_buf[MU_MAX_FMT]; - mu_Id number_edit; - /* stacks */ - mu_stack(char, MU_COMMANDLIST_SIZE) command_list; - mu_stack(mu_Container*, MU_ROOTLIST_SIZE) root_list; - mu_stack(mu_Container*, MU_CONTAINERSTACK_SIZE) container_stack; - mu_stack(mu_Rect, MU_CLIPSTACK_SIZE) clip_stack; - mu_stack(mu_Id, MU_IDSTACK_SIZE) id_stack; - mu_stack(mu_Layout, MU_LAYOUTSTACK_SIZE) layout_stack; - /* retained state pools */ - mu_PoolItem container_pool[MU_CONTAINERPOOL_SIZE]; - mu_Container containers[MU_CONTAINERPOOL_SIZE]; - mu_PoolItem treenode_pool[MU_TREENODEPOOL_SIZE]; - /* input state */ - mu_Vec2 mouse_pos; - mu_Vec2 last_mouse_pos; - mu_Vec2 mouse_delta; - mu_Vec2 scroll_delta; - int mouse_down; - int mouse_pressed; - int key_down; - int key_pressed; - char input_text[256]; -}; - - -mu_Vec2 mu_vec2(int x, int y); -mu_Rect mu_rect(int x, int y, int w, int h); -mu_Color mu_color(int r, int g, int b, int a); - -void mu_init(mu_Context *ctx); -void mu_begin(mu_Context *ctx); -void mu_end(mu_Context *ctx); -void mu_set_focus(mu_Context *ctx, mu_Id id); -mu_Id mu_get_id(mu_Context *ctx, const void *data, int size); -void mu_push_id(mu_Context *ctx, const void *data, int size); -void mu_pop_id(mu_Context *ctx); -void mu_push_clip_rect(mu_Context *ctx, mu_Rect rect); -void mu_pop_clip_rect(mu_Context *ctx); -mu_Rect mu_get_clip_rect(mu_Context *ctx); -int mu_check_clip(mu_Context *ctx, mu_Rect r); -mu_Container* mu_get_current_container(mu_Context *ctx); -mu_Container* mu_get_container(mu_Context *ctx, const char *name); -void mu_bring_to_front(mu_Context *ctx, mu_Container *cnt); - -int mu_pool_init(mu_Context *ctx, mu_PoolItem *items, int len, mu_Id id); -int mu_pool_get(mu_Context *ctx, mu_PoolItem *items, int len, mu_Id id); -void mu_pool_update(mu_Context *ctx, mu_PoolItem *items, int idx); - -void mu_input_mousemove(mu_Context *ctx, int x, int y); -void mu_input_mousedown(mu_Context *ctx, int x, int y, int btn); -void mu_input_mouseup(mu_Context *ctx, int x, int y, int btn); -void mu_input_scroll(mu_Context *ctx, int x, int y); -void mu_input_keydown(mu_Context *ctx, int key); -void mu_input_keyup(mu_Context *ctx, int key); -void mu_input_text(mu_Context *ctx, const char *text); - -mu_Command* mu_push_command(mu_Context *ctx, int type, int size); -int mu_next_command(mu_Context *ctx, mu_Command **cmd); -void mu_set_clip(mu_Context *ctx, mu_Rect rect); -void mu_draw_rect(mu_Context *ctx, mu_Rect rect, mu_Color color); -void mu_draw_box(mu_Context *ctx, mu_Rect rect, mu_Color color); -void mu_draw_text(mu_Context *ctx, mu_Font font, const char *str, int len, mu_Vec2 pos, mu_Color color); -void mu_draw_icon(mu_Context *ctx, int id, mu_Rect rect, mu_Color color); - -void mu_layout_row(mu_Context *ctx, int items, const int *widths, int height); -void mu_layout_width(mu_Context *ctx, int width); -void mu_layout_height(mu_Context *ctx, int height); -void mu_layout_begin_column(mu_Context *ctx); -void mu_layout_end_column(mu_Context *ctx); -void mu_layout_set_next(mu_Context *ctx, mu_Rect r, int relative); -mu_Rect mu_layout_next(mu_Context *ctx); - -void mu_draw_control_frame(mu_Context *ctx, mu_Id id, mu_Rect rect, int colorid, int opt); -void mu_draw_control_text(mu_Context *ctx, const char *str, mu_Rect rect, int colorid, int opt); -int mu_mouse_over(mu_Context *ctx, mu_Rect rect); -void mu_update_control(mu_Context *ctx, mu_Id id, mu_Rect rect, int opt); - -#define mu_button(ctx, label) mu_button_ex(ctx, label, 0, MU_OPT_ALIGNCENTER) -#define mu_textbox(ctx, buf, bufsz) mu_textbox_ex(ctx, buf, bufsz, 0) -#define mu_slider(ctx, value, lo, hi) mu_slider_ex(ctx, value, lo, hi, 0, MU_SLIDER_FMT, MU_OPT_ALIGNCENTER) -#define mu_number(ctx, value, step) mu_number_ex(ctx, value, step, MU_SLIDER_FMT, MU_OPT_ALIGNCENTER) -#define mu_header(ctx, label) mu_header_ex(ctx, label, 0) -#define mu_begin_treenode(ctx, label) mu_begin_treenode_ex(ctx, label, 0) -#define mu_begin_window(ctx, title, rect) mu_begin_window_ex(ctx, title, rect, 0) -#define mu_begin_panel(ctx, name) mu_begin_panel_ex(ctx, name, 0) - -void mu_text(mu_Context *ctx, const char *text); -void mu_label(mu_Context *ctx, const char *text); -int mu_button_ex(mu_Context *ctx, const char *label, int icon, int opt); -int mu_checkbox(mu_Context *ctx, const char *label, int *state); -int mu_textbox_raw(mu_Context *ctx, char *buf, int bufsz, mu_Id id, mu_Rect r, int opt); -int mu_textbox_ex(mu_Context *ctx, char *buf, int bufsz, int opt); -int mu_slider_ex(mu_Context *ctx, mu_Real *value, mu_Real low, mu_Real high, mu_Real step, const char *fmt, int opt); -int mu_number_ex(mu_Context *ctx, mu_Real *value, mu_Real step, const char *fmt, int opt); -int mu_header_ex(mu_Context *ctx, const char *label, int opt); -int mu_begin_treenode_ex(mu_Context *ctx, const char *label, int opt); -void mu_end_treenode(mu_Context *ctx); -int mu_begin_window_ex(mu_Context *ctx, const char *title, mu_Rect rect, int opt); -void mu_end_window(mu_Context *ctx); -void mu_open_popup(mu_Context *ctx, const char *name); -int mu_begin_popup(mu_Context *ctx, const char *name); -void mu_end_popup(mu_Context *ctx); -void mu_begin_panel_ex(mu_Context *ctx, const char *name, int opt); -void mu_end_panel(mu_Context *ctx); - -#endif blob - 77bc7524f26559f5bbcff3aebdc67c26b54a85fc blob + 29c6ef524419d9497cd8d3aba9f62a8d70c174d9 --- src/aproc.c +++ src/aproc.c @@ -7,24 +7,27 @@ #include "aproc.h" #define SR 48000 -#define TAIL (SR / 10) +#define TAIL (SR / 5) #define REF_RING (APROC_FRAME * 32) +#define REF_MAX (APROC_FRAME * 8) +#define REF_TARGET (APROC_FRAME * 2) static struct { SpeexEchoState *echo; SpeexPreprocessState *pre; int16_t ring[REF_RING]; atomic_size_t w, r; + int bypass; } P; static size_t ref_avail(void) { return atomic_load_explicit(&P.w, memory_order_acquire) - - atomic_load_explicit(&P.r, memory_order_relaxed); + - atomic_load_explicit(&P.r, memory_order_relaxed); } static void ref_write(const int16_t *src, size_t n) { size_t w = atomic_load_explicit(&P.w, memory_order_relaxed); size_t r = atomic_load_explicit(&P.r, memory_order_acquire); - if (REF_RING - (w - r) < n) return; /* full: drop, silently */ + if (REF_RING - (w - r) < n) return; for (size_t i = 0; i < n; i++) P.ring[(w + i) % REF_RING] = src[i]; atomic_store_explicit(&P.w, w + n, memory_order_release); } @@ -38,6 +41,7 @@ static size_t ref_read(int16_t *dst, size_t n) { int aproc_init(void) { atomic_store(&P.w, 0); atomic_store(&P.r, 0); + P.bypass = getenv("MILNA_NO_APROC") != NULL; P.echo = speex_echo_state_init(APROC_FRAME, TAIL); if (!P.echo) return -1; int sr = SR; @@ -47,10 +51,10 @@ int aproc_init(void) { if (!P.pre) return -2; speex_preprocess_ctl(P.pre, SPEEX_PREPROCESS_SET_ECHO_STATE, P.echo); - int on = 1, off = 0; - float agc_target = 8000.0f; + int on = 1; + float agc_target = 8000.0f; int agc_max_gain = 20; - int agc_incr = 6, agc_decr = -10; + int agc_incr = 6, agc_decr = -10; speex_preprocess_ctl(P.pre, SPEEX_PREPROCESS_SET_AGC, &on); speex_preprocess_ctl(P.pre, SPEEX_PREPROCESS_SET_AGC_LEVEL, &agc_target); speex_preprocess_ctl(P.pre, SPEEX_PREPROCESS_SET_AGC_MAX_GAIN, &agc_max_gain); @@ -60,18 +64,24 @@ int aproc_init(void) { int sup = -25, sup_active = -10; speex_preprocess_ctl(P.pre, SPEEX_PREPROCESS_SET_ECHO_SUPPRESS, &sup); speex_preprocess_ctl(P.pre, SPEEX_PREPROCESS_SET_ECHO_SUPPRESS_ACTIVE, - &sup_active); + &sup_active); speex_preprocess_ctl(P.pre, SPEEX_PREPROCESS_SET_DENOISE, &on); - (void)off; return 0; } void aproc_capture(int16_t *pcm) { - if (getenv("MILNA_NO_APROC")) return; + if (P.bypass) return; - int16_t ref[APROC_FRAME], clean[APROC_FRAME]; - size_t got = ref_read(ref, APROC_FRAME); - if (got == APROC_FRAME) { + size_t avail = ref_avail(); + if (avail > REF_MAX) { + size_t r = atomic_load_explicit(&P.r, memory_order_relaxed); + atomic_store_explicit(&P.r, r + (avail - REF_TARGET), + memory_order_release); + avail = REF_TARGET; + } + if (avail >= APROC_FRAME) { + int16_t ref[APROC_FRAME], clean[APROC_FRAME]; + ref_read(ref, APROC_FRAME); speex_echo_cancellation(P.echo, pcm, ref, clean); memcpy(pcm, clean, sizeof(clean)); } @@ -80,7 +90,7 @@ void aproc_capture(int16_t *pcm) { } void aproc_playback(const int16_t *pcm) { - if (getenv("MILNA_NO_APROC")) return; + if (P.bypass) return; ref_write(pcm, APROC_FRAME); } blob - 0996740ab5069ded85622c29466c8b130b642c12 blob + 78091faeb1fa6a86b408a1fc6b7c021884a48752 --- src/aproc.h +++ src/aproc.h @@ -2,7 +2,7 @@ #define APROC_H #include -#define APROC_FRAME 480 /* 10ms @ 48k — process in these units */ +#define APROC_FRAME 480 int aproc_init(void); void aproc_free(void); blob - e14898a920965090469701ef6952ca3b63a750fa blob + 49aeb5976a05c74d30a9ce9e303bd3f81fac83ff --- src/audio.c +++ src/audio.c @@ -11,160 +11,163 @@ #define SR 48000 #define CH 1 -#define FRAME 480 /* 10ms @ 48k — opus frame */ +#define FRAME 480 #define MAX_PKT 4000 -#define RING_SAMP (FRAME * 64) /* ~1.3s of buffered playback max */ +#define RING_SAMP (FRAME * 64) static struct { - ma_device cap_dev; /* capture-only device */ - ma_device play_dev; /* playback-only device */ + ma_device cap_dev; + ma_device play_dev; OpusEncoder *enc; - OpusDecoder *dec[16]; /* one decoder per sender slot */ + OpusDecoder *dec[16]; - int last_seq[16], expect_seq[16], have_seq[16]; /* FEC loss detect */ + int expect_seq[16], have_seq[16]; audio_sink_fn sink; - audio_sink_fn share_sink; /* screenshare/system audio send path */ + audio_sink_fn share_sink; void *user; atomic_int muted; - atomic_int deafened; /* silence ALL incoming audio */ - atomic_int reset_slot[16]; /* undeafen: clear slot state on udp thread */ - atomic_int gain[32]; /* 0-15 voice vol, 16-31 screenshare vol (percent) */ + atomic_int deafened; + atomic_int reset_slot[32]; + + atomic_int gain[32]; int gain_init[32]; _Atomic int mix[RING_SAMP]; - atomic_size_t r; /* global read cursor (samples) */ - size_t wcur[32]; /* per-slot write cursors (0-15 voice, 16-31 share) */ + atomic_size_t r; + size_t wcur[32]; - /* device selection (guarded by being main/ui-thread only) */ ma_context ctx; int ctx_up; ma_device_id cap_id, play_id; int have_cap_id, have_play_id; char cap_name[64], play_name[64]; - /* capture accumulator */ int16_t cap[FRAME]; int cap_n; - int started; + atomic_int started; } A; -static int jb_min, jb_def, jb_max, jb_shrink_margin, jb_win; -static void jb_init_profile(void) { - jb_min = FRAME * 4; /* 40ms floor — never thinner than this */ - jb_def = FRAME * 7; /* 70ms start — comfortable opening cushion */ - jb_max = FRAME * 20; /* 200ms ceiling under heavy bufferbloat */ - jb_shrink_margin = 5; /* need 50ms consistent surplus to shrink */ - jb_win = 750; /* ~7.5s adaptation: calm, shrinks rarely */ -} +enum { + JB_MIN = FRAME * 4, + JB_DEF = FRAME * 7, + JB_MAX = FRAME * 20, + JB_SHRINK_MARGIN = 5, + JB_WIN = 750, + JB_MAXAHEAD = JB_MAX + FRAME * 5 +}; -#define MIX_LEAD_MIN (FRAME * 2) /* 20ms floor (was 10 — too thin for - * bursty links; caused resync glitch) */ -#define MIX_LEAD_DEF (FRAME * 4) /* 40ms starting lead per sender */ -#define MIX_LEAD_MAX (FRAME * 10) /* 100ms ceiling under heavy jitter */ -#define MIX_MAXAHEAD (FRAME * 10) /* hard backlog cap */ -#define MIX_WIN 500 /* adaptation window: ~5s (slower, calmer) */ - static struct { - size_t lead[32]; /* current target lead, samples */ - long minsl[32]; /* min slack seen this window */ - int cnt[32]; /* packets this window */ - int repos[32]; /* repositions (audible clicks) */ + size_t lead[32]; + long minsl[32]; + int cnt[32]; + int repos[32]; + int shrink[32]; + int drain[32]; } J; static void mix_add(int slot, const int16_t *pcm, size_t n) { size_t r = atomic_load_explicit(&A.r, memory_order_acquire); size_t w = A.wcur[slot]; int fresh = 0; - if (J.lead[slot] == 0) { J.lead[slot] = jb_def; - J.minsl[slot] = (long)RING_SAMP; fresh = 1; } + if (J.lead[slot] == 0) { + J.lead[slot] = JB_DEF; + J.minsl[slot] = (long)RING_SAMP; + fresh = 1; + } long slack = (long)(w - r); - int reposition = fresh || slack < (long)FRAME /* underran (late) */ - || slack > (long)MIX_MAXAHEAD; /* ran away */ + + if (!fresh) { + if (J.drain[slot]) { + if (slack > (long)J.lead[slot]) return; + J.drain[slot] = 0; + } else if (slack > JB_MAXAHEAD) { + J.drain[slot] = 1; + J.repos[slot]++; + return; + } + } + + int reposition = fresh || slack < (long)(FRAME * 2); if (reposition) { - if (!fresh && slack < (long)FRAME && J.lead[slot] < jb_max) { - J.lead[slot] += FRAME; /* late: ask for more cushion */ + if (!fresh && J.lead[slot] < (size_t)JB_MAX) { + J.lead[slot] += FRAME; J.repos[slot]++; } w = r + J.lead[slot]; slack = (long)J.lead[slot]; J.minsl[slot] = (long)RING_SAMP; J.cnt[slot] = 0; + J.shrink[slot] = 0; } if (slack < J.minsl[slot]) J.minsl[slot] = slack; - if (++J.cnt[slot] >= jb_win) { + if (++J.cnt[slot] >= JB_WIN) { J.cnt[slot] = 0; - if (J.minsl[slot] > (long)(J.lead[slot] + jb_shrink_margin * FRAME) && - J.lead[slot] > jb_min) { + if (J.minsl[slot] > (long)(J.lead[slot] + JB_SHRINK_MARGIN * FRAME) && + J.lead[slot] > (size_t)JB_MIN) + J.shrink[slot] = 1; + J.minsl[slot] = (long)RING_SAMP; + } + + if (J.shrink[slot] && J.lead[slot] > (size_t)JB_MIN) { + int quiet = 1; + for (size_t i = 0; i < n; i++) + if (pcm[i] > 300 || pcm[i] < -300) { quiet = 0; break; } + if (quiet) { + J.shrink[slot] = 0; J.lead[slot] -= FRAME; - J.minsl[slot] = (long)RING_SAMP; - return; /* frame intentionally not written */ + return; } - J.minsl[slot] = (long)RING_SAMP; } int g = atomic_load_explicit(&A.gain[slot], memory_order_relaxed); - if (g == 0 && !A.gain_init[slot]) g = 100; /* default unity */ + if (g == 0 && !A.gain_init[slot]) g = 100; for (size_t i = 0; i < n; i++) { int v = pcm[i] * g / 100; atomic_fetch_add_explicit(&A.mix[(w + i) % RING_SAMP], v, - memory_order_relaxed); + memory_order_relaxed); } A.wcur[slot] = w + n; } static void mix_read(int16_t *dst, size_t n) { size_t r = atomic_load_explicit(&A.r, memory_order_relaxed); - int loud = 0; for (size_t i = 0; i < n; i++) { int v = atomic_exchange_explicit(&A.mix[(r + i) % RING_SAMP], 0, - memory_order_relaxed); - if (v > 500 || v < -500) loud = 1; + memory_order_relaxed); dst[i] = (int16_t)(v > 32767 ? 32767 : v < -32768 ? -32768 : v); } atomic_store_explicit(&A.r, r + n, memory_order_release); - static int announced; - if (loud && !announced) { - fprintf(stderr, "[milna] playing remote audio\n"); - announced = 1; - } } static void on_frame(int16_t *pcm) { + aproc_capture(pcm); if (atomic_load_explicit(&A.muted, memory_order_relaxed)) return; - aproc_capture(pcm); /* FRAME == APROC_FRAME == 10ms now */ - { - static int mic_live; - if (!mic_live) - for (int i = 0; i < FRAME; i++) - if (pcm[i] > 500 || pcm[i] < -500) { - fprintf(stderr, "[milna] mic capturing (signal " - "detected)\n"); - mic_live = 1; - break; - } - } uint8_t pkt[MAX_PKT]; int n = opus_encode(A.enc, pcm, FRAME, pkt, sizeof(pkt)); if (n > 0 && A.sink) A.sink(pkt, n, A.user); } static OpusEncoder *share_enc; -static OpusDecoder *share_dec[16]; /* per-slot stereo music decoders */ +static OpusDecoder *share_dec[16]; +static int share_expect_seq[16], share_have_seq[16]; static _Atomic int share_audio_on; int audio_share_start(void) { - if (share_enc) return 0; - int err; - share_enc = opus_encoder_create(SR, 1, OPUS_APPLICATION_AUDIO, &err); - if (err != OPUS_OK) { share_enc = NULL; return -1; } - opus_encoder_ctl(share_enc, OPUS_SET_BITRATE(32000)); - opus_encoder_ctl(share_enc, OPUS_SET_SIGNAL(OPUS_SIGNAL_MUSIC)); - opus_encoder_ctl(share_enc, OPUS_SET_INBAND_FEC(1)); - opus_encoder_ctl(share_enc, OPUS_SET_PACKET_LOSS_PERC(10)); + if (!share_enc) { + int err; + share_enc = opus_encoder_create(SR, 1, OPUS_APPLICATION_AUDIO, &err); + if (err != OPUS_OK) { share_enc = NULL; return -1; } + opus_encoder_ctl(share_enc, OPUS_SET_BITRATE(32000)); + opus_encoder_ctl(share_enc, OPUS_SET_SIGNAL(OPUS_SIGNAL_MUSIC)); + opus_encoder_ctl(share_enc, OPUS_SET_INBAND_FEC(1)); + opus_encoder_ctl(share_enc, OPUS_SET_PACKET_LOSS_PERC(10)); + fprintf(stderr, "[milna] share audio: encoder up (mono 32k music)\n"); + } else { + opus_encoder_ctl(share_enc, OPUS_RESET_STATE); + } atomic_store(&share_audio_on, 1); - fprintf(stderr, "[milna] share audio: encoder up (mono 32k music)\n"); return 0; } @@ -183,28 +186,18 @@ int audio_share_get_gain(int slot) { void audio_share_stop(void) { atomic_store(&share_audio_on, 0); - if (share_enc) { opus_encoder_destroy(share_enc); share_enc = NULL; } - for (int i = 0; i < 16; i++) - if (share_dec[i]) { opus_decoder_destroy(share_dec[i]); share_dec[i] = NULL; } } void audio_share_feed(const int16_t *stereo, int frames) { if (!atomic_load_explicit(&share_audio_on, memory_order_relaxed)) return; if (!share_enc || frames != FRAME) return; - /* downmix interleaved stereo capture -> mono for the 32k mono encoder */ + int16_t mono[FRAME]; for (int i = 0; i < FRAME; i++) mono[i] = (int16_t)(((int)stereo[2*i] + stereo[2*i+1]) / 2); uint8_t pkt[MAX_PKT]; int n = opus_encode(share_enc, mono, FRAME, pkt, sizeof(pkt)); - if (n > 0 && A.share_sink) { - static int sent; - if (sent == 0) - fprintf(stderr, "[milna] share audio: first frame SENT (%d b)\n", n); - if ((++sent % 500) == 0) /* ~every 5s */ - fprintf(stderr, "[milna] share audio: sent %d frames\n", sent); - A.share_sink(pkt, n, A.user); - } + if (n > 0 && A.share_sink) A.share_sink(pkt, n, A.user); } static void cap_cb(ma_device *d, void *out, const void *in, ma_uint32 nframes) { @@ -215,29 +208,23 @@ static void cap_cb(ma_device *d, void *out, const void if (A.cap_n == FRAME) { on_frame(A.cap); A.cap_n = 0; } } } +static int16_t play_ref[APROC_FRAME]; +static int play_ref_n; + static void play_cb(ma_device *d, void *out, const void *in, ma_uint32 nframes) { (void)d; (void)in; int16_t *spk = (int16_t *)out; - static int logged; - if (!logged) { - fprintf(stderr, "[milna] playback callback: %u frames/call\n", - (unsigned)nframes); - logged = 1; - } - mix_read(spk, nframes); /* drains nframes samples; grid-independent */ + mix_read(spk, nframes); if (atomic_load_explicit(&A.deafened, memory_order_relaxed)) memset(spk, 0, (size_t)nframes * sizeof(int16_t)); - static int16_t ref[APROC_FRAME]; - static int ref_n = 0; for (ma_uint32 i = 0; i < nframes; i++) { - ref[ref_n++] = spk[i]; - if (ref_n == APROC_FRAME) { aproc_playback(ref); ref_n = 0; } + play_ref[play_ref_n++] = spk[i]; + if (play_ref_n == APROC_FRAME) { aproc_playback(play_ref); play_ref_n = 0; } } } -/* ---- public api ---- */ static int ensure_ctx(void) { if (A.ctx_up) return 0; if (ma_context_init(NULL, 0, NULL, &A.ctx) != MA_SUCCESS) return -1; @@ -264,14 +251,14 @@ static int select_apply(int capture, const ma_device_i return 0; if (capture) { if (info) { A.cap_id = info->id; A.have_cap_id = 1; - snprintf(A.cap_name, 64, "%s", info->name); } + snprintf(A.cap_name, 64, "%s", info->name); } else { A.have_cap_id = 0; A.cap_name[0] = 0; } } else { if (info) { A.play_id = info->id; A.have_play_id = 1; - snprintf(A.play_name, 64, "%s", info->name); } + snprintf(A.play_name, 64, "%s", info->name); } else { A.have_play_id = 0; A.play_name[0] = 0; } } - if (A.started) { /* live switch: restart on new device */ + if (atomic_load_explicit(&A.started, memory_order_relaxed)) { audio_sink_fn s = A.sink; void *u = A.user; int muted = atomic_load(&A.muted); audio_stop(); @@ -279,7 +266,7 @@ static int select_apply(int capture, const ma_device_i atomic_store(&A.muted, muted); if (r != 0) fprintf(stderr, "[milna] AUDIO RESTART FAILED (%d) — " - "no devices running\n", r); + "no devices running\n", r); return r; } return 0; @@ -308,7 +295,7 @@ int audio_select_by_name(int capture, const char *name for (int i = 0; i < n; i++) if (!strcmp(src[i].name, name)) return select_apply(capture, &src[i]); - return -1; /* device not present right now; default stays */ + return -1; } const char *audio_sel_name(int capture) { @@ -317,17 +304,18 @@ const char *audio_sel_name(int capture) { int audio_start(audio_sink_fn sink, void *user) { int err; - jb_init_profile(); /* fixed stability-leaning jitter bounds */ memset(&A.cap, 0, sizeof(A.cap)); + memset(play_ref, 0, sizeof(play_ref)); play_ref_n = 0; A.sink = sink; A.user = user; A.cap_n = 0; atomic_store(&A.muted, 0); A.enc = opus_encoder_create(SR, CH, OPUS_APPLICATION_VOIP, &err); if (err != OPUS_OK) return -1; - opus_encoder_ctl(A.enc, OPUS_SET_BITRATE(32000)); /* fuller voice */ + opus_encoder_ctl(A.enc, OPUS_SET_BITRATE(32000)); opus_encoder_ctl(A.enc, OPUS_SET_SIGNAL(OPUS_SIGNAL_VOICE)); opus_encoder_ctl(A.enc, OPUS_SET_INBAND_FEC(1)); - opus_encoder_ctl(A.enc, OPUS_SET_PACKET_LOSS_PERC(25)); + opus_encoder_ctl(A.enc, OPUS_SET_PACKET_LOSS_PERC(15)); + opus_encoder_ctl(A.enc, OPUS_SET_DTX(0)); if (aproc_init() != 0) return -5; atomic_store(&A.r, 0); @@ -336,6 +324,9 @@ int audio_start(audio_sink_fn sink, void *user) { memset(&J, 0, sizeof(J)); for (int i = 0; i < 32; i++) { A.gain[i] = 100; A.gain_init[i] = 1; } + for (int i = 0; i < 32; i++) + atomic_store_explicit(&A.reset_slot[i], 1, memory_order_relaxed); + if (ensure_ctx() != 0) return -7; ma_device_config cc = ma_device_config_init(ma_device_type_capture); if (A.have_cap_id) cc.capture.pDeviceID = &A.cap_id; @@ -360,76 +351,76 @@ int audio_start(audio_sink_fn sink, void *user) { if (ma_device_start(&A.cap_dev) != MA_SUCCESS) return -4; if (ma_device_start(&A.play_dev) != MA_SUCCESS) return -4; - A.started = 1; + atomic_store_explicit(&A.started, 1, memory_order_release); return 0; } -/* incoming screenshare/system audio (wire type 4): decode stereo music and - * mix into playback. separate decoders from voice; downmix to mono into the - * existing mono mix ring (the mixer is mono). */ -void audio_share_recv(int slot, const uint8_t *opus, int len) { - if (slot < 0 || slot >= 16) return; - if (!share_dec[slot]) { - int err; - share_dec[slot] = opus_decoder_create(SR, 1, &err); - if (err != OPUS_OK) { share_dec[slot] = NULL; return; } - } - int16_t mono[FRAME]; - int n = opus_decode(share_dec[slot], opus, len, mono, FRAME, 0); - if (n <= 0) return; - { - static int rcvd; - if (rcvd == 0) - fprintf(stderr, "[milna] share audio: first frame RECEIVED " - "from slot %d (%d samples)\n", slot, n); - if ((++rcvd % 500) == 0) - fprintf(stderr, "[milna] share audio: received %d frames\n", rcvd); - } - /* mix into a SEPARATE slot (slot+16) so screenshare audio never shares - * a write cursor with the sender's voice (that collision = static). */ - mix_add(slot + 16, mono, (size_t)n); -} +#define MAXDEC 5760 -void audio_recv_packet(int slot, int seq, const uint8_t *opus, int len) { - if (slot < 0 || slot >= 16) return; - /* pending reset from an undeafen: clear this slot's jitter + decoder - * here on the udp thread so we never reset a decoder mid-use. */ - if (atomic_exchange_explicit(&A.reset_slot[slot], 0, memory_order_relaxed)) { - J.lead[slot] = 0; J.minsl[slot] = 0; J.cnt[slot] = 0; - A.wcur[slot] = 0; A.have_seq[slot] = 0; - if (A.dec[slot]) opus_decoder_ctl(A.dec[slot], OPUS_RESET_STATE); +static int recv_decode(OpusDecoder **dp, int mixslot, int *have, int *expect, + int seq, const uint8_t *opus, int len, int16_t *pcm, int maxdec) { + if (atomic_exchange_explicit(&A.reset_slot[mixslot], 0, memory_order_relaxed)) { + J.lead[mixslot] = 0; J.minsl[mixslot] = 0; J.cnt[mixslot] = 0; + J.shrink[mixslot] = 0; J.drain[mixslot] = 0; + A.wcur[mixslot] = 0; *have = 0; + if (*dp) opus_decoder_ctl(*dp, OPUS_RESET_STATE); } - if (!A.dec[slot]) { + if (!*dp) { int err; - A.dec[slot] = opus_decoder_create(SR, CH, &err); - if (err != OPUS_OK) { A.dec[slot] = NULL; return; } + *dp = opus_decoder_create(SR, 1, &err); + if (err != OPUS_OK) { *dp = NULL; return 0; } } - enum { MAXDEC = 5760 }; /* 120ms @ 48k, opus maximum */ - static int16_t pcm[MAXDEC]; /* udp thread only */ - - if (A.have_seq[slot] && seq >= 0) { - int gap = seq - A.expect_seq[slot]; + if (*have && seq >= 0) { + int gap = seq - *expect; if (gap >= 2 && gap <= 5) { for (int g = 0; g < gap - 1; g++) { - int cn = opus_decode(A.dec[slot], NULL, 0, pcm, FRAME, 0); - if (cn > 0) mix_add(slot, pcm, (size_t)cn); + int cn = opus_decode(*dp, NULL, 0, pcm, FRAME, 0); + if (cn > 0) mix_add(mixslot, pcm, (size_t)cn); } } if (gap == 1 || (gap >= 2 && gap <= 5)) { - int fn = opus_decode(A.dec[slot], opus, len, pcm, MAXDEC, 1); - if (fn > 0) mix_add(slot, pcm, (size_t)fn); + int fn = opus_decode(*dp, opus, len, pcm, maxdec, 1); + if (fn > 0) mix_add(mixslot, pcm, (size_t)fn); } } + int n = opus_decode(*dp, opus, len, pcm, maxdec, 0); + if (n <= 0) return 0; + *expect = seq + 1; + *have = 1; + mix_add(mixslot, pcm, (size_t)n); + return n; +} - int n = opus_decode(A.dec[slot], opus, len, pcm, MAXDEC, 0); - if (n <= 0) return; - A.expect_seq[slot] = seq + 1; - A.have_seq[slot] = 1; - mix_add(slot, pcm, (size_t)n); +void audio_share_recv(int slot, int seq, const uint8_t *opus, int len) { + int16_t mono[FRAME]; + if (slot < 0 || slot >= 16) return; + if (!atomic_load_explicit(&A.started, memory_order_acquire)) return; + int n = recv_decode(&share_dec[slot], slot + 16, &share_have_seq[slot], + &share_expect_seq[slot], seq, opus, len, mono, FRAME); + if (n > 0) { + static int rcvd; + if (!rcvd++) + fprintf(stderr, "[milna] share audio: first frame RECEIVED " + "from slot %d (%d samples)\n", slot, n); + } } +void audio_recv_packet(int slot, int seq, const uint8_t *opus, int len) { + static int16_t pcm[MAXDEC]; + if (slot < 0 || slot >= 16) return; + if (!atomic_load_explicit(&A.started, memory_order_acquire)) return; + recv_decode(&A.dec[slot], slot, &A.have_seq[slot], &A.expect_seq[slot], + seq, opus, len, pcm, MAXDEC); +} + void audio_set_muted(int m) { atomic_store_explicit(&A.muted, m, memory_order_relaxed); } +void audio_reset_slot(int slot) { + if (slot < 0 || slot >= 16) return; + atomic_store_explicit(&A.reset_slot[slot], 1, memory_order_relaxed); + atomic_store_explicit(&A.reset_slot[slot + 16], 1, memory_order_relaxed); +} + void audio_set_deafened(int d) { int was = atomic_load_explicit(&A.deafened, memory_order_relaxed); atomic_store_explicit(&A.deafened, d, memory_order_relaxed); @@ -448,17 +439,15 @@ void audio_set_gain(int slot, int percent) { int audio_get_gain(int slot) { if (slot < 0 || slot >= 16) return 100; return A.gain_init[slot] ? atomic_load_explicit(&A.gain[slot], - memory_order_relaxed) : 100; + memory_order_relaxed) : 100; } void audio_stop(void) { - if (!A.started) return; - A.started = 0; + if (!atomic_exchange_explicit(&A.started, 0, memory_order_acq_rel)) return; ma_device_uninit(&A.cap_dev); ma_device_uninit(&A.play_dev); if (A.enc) opus_encoder_destroy(A.enc); A.enc = NULL; - for (int i = 0; i < 16; i++) - if (A.dec[i]) { opus_decoder_destroy(A.dec[i]); A.dec[i] = NULL; } + aproc_free(); } blob - 408a48c76823efb76b1017247cb02e474b87196d blob + f8a46ba8d7c85a978d8025637f2faa8051766b69 --- src/audio.h +++ src/audio.h @@ -9,17 +9,18 @@ int audio_start(audio_sink_fn sink, void *user); void audio_stop(void); void audio_recv_packet(int slot, int seq, const uint8_t *opus, int len); +void audio_reset_slot(int slot); int audio_share_start(void); void audio_share_stop(void); void audio_share_feed(const int16_t *stereo, int frames); -void audio_share_recv(int slot, const uint8_t *opus, int len); +void audio_share_recv(int slot, int seq, const uint8_t *opus, int len); void audio_share_set_sink(audio_sink_fn fn); -void audio_share_set_gain(int slot, int pct); /* change screenshare volume */ +void audio_share_set_gain(int slot, int pct); int audio_share_get_gain(int slot); void audio_set_muted(int muted); -void audio_set_deafened(int deaf); +void audio_set_deafened(int deaf); void audio_set_gain(int slot, int percent); int audio_get_gain(int slot); blob - eaee93dda687494f6417bef4e99b09e14b3af610 blob + 8d6e89c79f0348ce16286b5f78c4a793a2569d2b --- src/cam.h +++ src/cam.h @@ -1,6 +1,6 @@ #ifndef CAM_H #define CAM_H -int cam_start(void); /* 0 ok; may prompt for camera permission */ +int cam_start(void); void cam_stop(void); int cam_active(void); #endif blob - 2c94a2f60fba43f3fba947f1334629d09c2951ac blob + 8a00e30f47107ac44b8f265d14e2cd4727f6aafe --- src/cam_mac.m +++ src/cam_mac.m @@ -31,8 +31,8 @@ static struct { @implementation MilnaCamSink - (void)captureOutput:(AVCaptureOutput *)output - didOutputSampleBuffer:(CMSampleBufferRef)sb - fromConnection:(AVCaptureConnection *)conn { + didOutputSampleBuffer:(CMSampleBufferRef)sb + fromConnection:(AVCaptureConnection *)conn { (void)output; (void)conn; if (!C.active) return; CVImageBufferRef pb = CMSampleBufferGetImageBuffer(sb); @@ -43,8 +43,8 @@ static struct { if (w < 16 || h < 16) return; if (!C.enc_up) { - C.hi = vid_enc_new(w, h, CAM_FPS, milna_vbitrate(CAM_KBPS)); - C.lo = vid_enc_new(w / 2, h / 2, CAM_FPS, milna_vbitrate(CAM_KBPS_LO)); + C.hi = vid_enc_new(w, h, CAM_FPS, CAM_KBPS); + C.lo = vid_enc_new(w / 2, h / 2, CAM_FPS, CAM_KBPS_LO); C.frame = malloc((size_t)w * h * 4); C.frame_lo = malloc((size_t)(w / 2) * (h / 2) * 4); C.enc = malloc(1024 * 1024); @@ -56,52 +56,41 @@ static struct { C.w = w; C.h = h; C.enc_up = 1; C.first = 1; CLOG("camera up: %dx%d@%d", w, h, CAM_FPS); } - if (w != C.w || h != C.h) return; /* mode changed mid-flight: skip */ + if (w != C.w || h != C.h) return; CVPixelBufferLockBaseAddress(pb, kCVPixelBufferLock_ReadOnly); const uint8_t *src = CVPixelBufferGetBaseAddress(pb); size_t stride = CVPixelBufferGetBytesPerRow(pb); for (int y = 0; y < C.h; y++) memcpy(C.frame + (size_t)y * C.w * 4, src + y * stride, - (size_t)C.w * 4); + (size_t)C.w * 4); CVPixelBufferUnlockBaseAddress(pb, kCVPixelBufferLock_ReadOnly); - ui_local_video(C.frame, C.w, C.h, 0); /* own tile shows own camera */ + ui_local_video(C.frame, C.w, C.h, 0); int key = 0; int force = C.first || net_keyframe_wanted(); C.first = 0; int n = vid_enc_frame(C.hi, C.frame, force, C.enc, 1024 * 1024, &key); - if (n > 0) net_send_video(C.enc, n, key, 0, 1); /* camera, high */ + if (n > 0) net_send_video(C.enc, n, key, 0, 1); - int lw = C.w / 2, lh = C.h / 2; - for (int y = 0; y < lh; y++) { - const uint8_t *r0 = C.frame + (size_t)(y * 2) * C.w * 4; - const uint8_t *r1 = r0 + (size_t)C.w * 4; - uint8_t *d = C.frame_lo + (size_t)y * lw * 4; - for (int x = 0; x < lw; x++) { - const uint8_t *a = r0 + x * 8, *b = a + 4; - const uint8_t *c2 = r1 + x * 8, *e = c2 + 4; - for (int k = 0; k < 4; k++) - d[x * 4 + k] = (uint8_t)((a[k] + b[k] + c2[k] + e[k]) >> 2); - } - } + bgra_half(C.frame, C.w, C.h, C.frame_lo); n = vid_enc_frame(C.lo, C.frame_lo, force, C.enc, 1024 * 1024, &key); - if (n > 0) net_send_video(C.enc, n, key, 0, 0); /* camera, low */ + if (n > 0) net_send_video(C.enc, n, key, 0, 0); } @end static void cam_session_start(void) { AVCaptureDevice *dev = - [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; + [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; if (!dev) { CLOG("no camera found"); C.active = 0; return; } NSError *err = nil; AVCaptureDeviceInput *in = - [AVCaptureDeviceInput deviceInputWithDevice:dev error:&err]; + [AVCaptureDeviceInput deviceInputWithDevice:dev error:&err]; if (!in) { CLOG("camera input: %s", - err ? err.localizedDescription.UTF8String : "failed"); + err ? err.localizedDescription.UTF8String : "failed"); C.active = 0; return; } @@ -120,7 +109,7 @@ static void cam_session_start(void) { [s addOutput:out]; [out release]; - if ([dev lockForConfiguration:NULL]) { /* cap to CAM_FPS if allowed */ + if ([dev lockForConfiguration:NULL]) { @try { dev.activeVideoMinFrameDuration = CMTimeMake(1, CAM_FPS); dev.activeVideoMaxFrameDuration = CMTimeMake(1, CAM_FPS); @@ -138,7 +127,7 @@ int cam_start(void) { if (!C.sink) C.sink = [MilnaCamSink new]; C.active = 1; [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo - completionHandler:^(BOOL ok) { + completionHandler:^(BOOL ok) { if (!ok) { CLOG("camera permission denied"); C.active = 0; @@ -157,7 +146,7 @@ void cam_stop(void) { [s stopRunning]; [s release]; } - if (C.q) dispatch_sync(C.q, ^{}); /* drain in-flight frame handlers */ + if (C.q) dispatch_sync(C.q, ^{}); if (C.enc_up) { vid_enc_del(C.hi); C.hi = NULL; vid_enc_del(C.lo); C.lo = NULL; blob - ec9ccd602bc5424fddfdd176ad968371d1343c8c blob + f1e76f864fc5cfe3e3b6ea16c9668641cc912f94 --- src/cam_win.c +++ src/cam_win.c @@ -24,33 +24,11 @@ static struct { volatile int run, active; } C; -/* NV12 (camera native) -> BGRA. cameras commonly deliver NV12 or YUY2; - * we request NV12 from the reader so this one path covers it. */ -static void nv12_to_bgra(const uint8_t *Y, const uint8_t *UV, - int w, int h, int stride, uint8_t *dst) { - for (int y = 0; y < h; y++) { - const uint8_t *yr = Y + (size_t)y * stride; - const uint8_t *uv = UV + (size_t)(y / 2) * stride; - uint8_t *d = dst + (size_t)y * w * 4; - for (int x = 0; x < w; x++) { - int c = yr[x] - 16; - int u = uv[(x & ~1)] - 128, v = uv[(x & ~1) + 1] - 128; - int r = (298 * c + 409 * v + 128) >> 8; - int g = (298 * c - 100 * u - 208 * v + 128) >> 8; - int b = (298 * c + 516 * u + 128) >> 8; - d[x*4] = (uint8_t)(b<0?0:b>255?255:b); - d[x*4+1] = (uint8_t)(g<0?0:g>255?255:g); - d[x*4+2] = (uint8_t)(r<0?0:r>255?255:r); - d[x*4+3] = 0xff; - } - } -} - static IMFSourceReader *open_camera(int *w, int *h) { IMFAttributes *cfg = NULL; MFCreateAttributes(&cfg, 1); IMFAttributes_SetGUID(cfg, &MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, - &MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID); + &MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID); IMFActivate **devs = NULL; UINT32 ndev = 0; if (FAILED(MFEnumDeviceSources(cfg, &devs, &ndev)) || ndev == 0) { @@ -75,21 +53,20 @@ static IMFSourceReader *open_camera(int *w, int *h) { } IMFMediaSource_Release(src); - /* request NV12 output (the reader inserts a converter if needed) */ IMFMediaType *mt = NULL; MFCreateMediaType(&mt); IMFMediaType_SetGUID(mt, &MF_MT_MAJOR_TYPE, &MFMediaType_Video); IMFMediaType_SetGUID(mt, &MF_MT_SUBTYPE, &MFVideoFormat_NV12); HRESULT hr = IMFSourceReader_SetCurrentMediaType(rd, - (DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM, NULL, mt); + (DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM, NULL, mt); IMFMediaType_Release(mt); if (FAILED(hr)) { CLOG("camera NV12 not accepted: 0x%lx", hr); - IMFSourceReader_Release(rd); return NULL; } + IMFSourceReader_Release(rd); return NULL; } IMFMediaType *cur = NULL; UINT32 cw = 640, ch = 480; if (SUCCEEDED(IMFSourceReader_GetCurrentMediaType(rd, - (DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM, &cur))) { + (DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM, &cur))) { UINT64 sz = 0; IMFMediaType_GetUINT64(cur, &MF_MT_FRAME_SIZE, &sz); cw = (UINT32)(sz >> 32); ch = (UINT32)sz; @@ -108,8 +85,8 @@ static DWORD WINAPI cam_main(LPVOID arg) { uint8_t *frame = malloc((size_t)w * h * 4); uint8_t *frame_lo = malloc((size_t)(w / 2) * (h / 2) * 4); uint8_t *enc = malloc(1024 * 1024); - vid_enc *hi = vid_enc_new(w, h, CAM_FPS, milna_vbitrate(CAM_KBPS)); - vid_enc *lo = vid_enc_new(w / 2, h / 2, CAM_FPS, milna_vbitrate(CAM_KBPS_LO)); + vid_enc *hi = vid_enc_new(w, h, CAM_FPS, CAM_KBPS); + vid_enc *lo = vid_enc_new(w / 2, h / 2, CAM_FPS, CAM_KBPS_LO); if (!frame || !frame_lo || !enc || !hi || !lo) { CLOG("cam init failed"); goto out; } CLOG("camera up: %dx%d@%d", w, h, CAM_FPS); @@ -118,7 +95,7 @@ static DWORD WINAPI cam_main(LPVOID arg) { DWORD flags = 0; LONGLONG ts = 0; DWORD sidx = 0; IMFSample *s = NULL; HRESULT hr = IMFSourceReader_ReadSample(rd, - (DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM, 0, &sidx, &flags, &ts, &s); + (DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM, 0, &sidx, &flags, &ts, &s); if (FAILED(hr)) break; if (!s) { if (flags & MF_SOURCE_READERF_ENDOFSTREAM) break; continue; } @@ -126,10 +103,10 @@ static DWORD WINAPI cam_main(LPVOID arg) { if (SUCCEEDED(IMFSample_ConvertToContiguousBuffer(s, &buf))) { BYTE *p; DWORD max, len; if (SUCCEEDED(IMFMediaBuffer_Lock(buf, &p, &max, &len))) { - nv12_to_bgra(p, p + (size_t)w * h, w, h, w, frame); + vid_nv12_to_bgra(p, w, p + (size_t)w * h, w, w, h, frame); IMFMediaBuffer_Unlock(buf); - ui_local_video(frame, w, h, 0); /* own tile preview */ + ui_local_video(frame, w, h, 0); int key = 0; int force = first || net_keyframe_wanted(); @@ -137,17 +114,7 @@ static DWORD WINAPI cam_main(LPVOID arg) { int n = vid_enc_frame(hi, frame, force, enc, 1024*1024, &key); if (n > 0) net_send_video(enc, n, key, 0, 1); - int lw = w / 2, lh = h / 2; - for (int y = 0; y < lh; y++) { - const uint8_t *r0 = frame + (size_t)(y*2)*w*4; - const uint8_t *r1 = r0 + (size_t)w*4; - uint8_t *d = frame_lo + (size_t)y*lw*4; - for (int x = 0; x < lw; x++) { - const uint8_t *a=r0+x*8,*b=a+4,*c2=r1+x*8,*e2=c2+4; - for (int k=0;k<4;k++) - d[x*4+k]=(uint8_t)((a[k]+b[k]+c2[k]+e2[k])>>2); - } - } + bgra_half(frame, w, h, frame_lo); n = vid_enc_frame(lo, frame_lo, force, enc, 1024*1024, &key); if (n > 0) net_send_video(enc, n, key, 0, 0); } blob - d031ad1ea19beb67497e7cd2936cafd93922caf6 blob + e353ebbd2f13b745092c624556f2f0affebe8323 --- src/cap_mac.m +++ src/cap_mac.m @@ -15,7 +15,7 @@ #define CAP_FPS 12 #define CAP_KBPS 1200 #define CAP_KBPS_LO 300 -#define CAP_MAXW 1280 /* scale capture down to at most this (gpu-side) */ +#define CAP_MAXW 1280 @interface MilnaCap : NSObject @end @@ -24,26 +24,26 @@ static struct { SCStream *stream; MilnaCap *sink; dispatch_queue_t q; - dispatch_queue_t aq; /* separate queue for audio output */ - vid_enc *venc; /* high layer: capture res */ - vid_enc *venc_lo; /* low layer: half res (simulcast) */ - uint8_t *frame; /* tight bgra staging */ - uint8_t *frame_lo; /* half-res staging */ - uint8_t *enc; /* encoded output staging */ + dispatch_queue_t aq; + vid_enc *venc; + vid_enc *venc_lo; + uint8_t *frame; + uint8_t *frame_lo; + uint8_t *enc; int w, h; int enc_up; int first; volatile int active; - int16_t aud[480 * 2]; /* interleaved stereo accumulator (CAP_AFRAME) */ + int16_t aud[480 * 2]; int aud_n; int audio_on; } C; -#define CAP_AFRAME 480 +#define CAP_AFRAME 480 @implementation MilnaCap - (void)stream:(SCStream *)stream - didOutputSampleBuffer:(CMSampleBufferRef)sb - ofType:(SCStreamOutputType)type { + didOutputSampleBuffer:(CMSampleBufferRef)sb + ofType:(SCStreamOutputType)type { (void)stream; if (type == SCStreamOutputTypeAudio) { if (!C.active || !CMSampleBufferIsValid(sb)) return; @@ -51,8 +51,8 @@ static struct { AudioBufferList *ablp = &ablx.l; CMBlockBufferRef block = NULL; OSStatus s = CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer( - sb, NULL, ablp, sizeof(ablx), NULL, NULL, - kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment, &block); + sb, NULL, ablp, sizeof(ablx), NULL, NULL, + kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment, &block); if (s != noErr || !block) { static int e1; if (!e1++) CLOG("audio buf extract failed: %d", (int)s); return; @@ -63,15 +63,15 @@ static struct { int nch = (int)abl.mBuffers[0].mNumberChannels; static int dbg; if (!dbg++) CLOG("AUDIO ARRIVING: %d buffers, ch=%d, frames=%d " - "(buf0 bytes=%u)", (int)ablp->mNumberBuffers, nch, - nframes, (unsigned)abl.mBuffers[0].mDataByteSize); + "(buf0 bytes=%u)", (int)ablp->mNumberBuffers, nch, + nframes, (unsigned)abl.mBuffers[0].mDataByteSize); if (f && nframes > 0) { for (int i = 0; i < nframes; i++) { float l, r; - if (ablp->mNumberBuffers >= 2) { /* planar stereo */ + if (ablp->mNumberBuffers >= 2) { l = ((const float *)ablp->mBuffers[0].mData)[i]; r = ((const float *)ablp->mBuffers[1].mData)[i]; - } else { /* interleaved */ + } else { l = f[i * nch]; r = nch > 1 ? f[i * nch + 1] : l; } @@ -91,15 +91,15 @@ static struct { if (type != SCStreamOutputTypeScreen || !C.active) return; if (!CMSampleBufferIsValid(sb)) return; CVPixelBufferRef pb = CMSampleBufferGetImageBuffer(sb); - if (!pb) return; /* status-only buffers (idle screen) have no image */ + if (!pb) return; int w = (int)CVPixelBufferGetWidth(pb) & ~1; int h = (int)CVPixelBufferGetHeight(pb) & ~1; if (w < 16 || h < 16) return; if (!C.enc_up) { - C.venc = vid_enc_new(w, h, CAP_FPS, milna_vbitrate(CAP_KBPS)); - C.venc_lo = vid_enc_new(w / 2, h / 2, CAP_FPS, milna_vbitrate(CAP_KBPS_LO)); + C.venc = vid_enc_new(w, h, CAP_FPS, CAP_KBPS); + C.venc_lo = vid_enc_new(w / 2, h / 2, CAP_FPS, CAP_KBPS_LO); if (!C.venc || !C.venc_lo) { CLOG("encoder init failed"); C.active = 0; return; } @@ -110,7 +110,7 @@ static struct { if (!C.frame || !C.enc) { C.active = 0; return; } CLOG("sharing %dx%d@%d", w, h, CAP_FPS); } - if (w != C.w || h != C.h) return; /* ignore size changes mid-stream */ + if (w != C.w || h != C.h) return; CVPixelBufferLockBaseAddress(pb, kCVPixelBufferLock_ReadOnly); const uint8_t *s = CVPixelBufferGetBaseAddress(pb); @@ -119,39 +119,26 @@ static struct { memcpy(C.frame + (size_t)y * C.w * 4, s + y * stride, (size_t)C.w * 4); CVPixelBufferUnlockBaseAddress(pb, kCVPixelBufferLock_ReadOnly); - ui_local_video(C.frame, C.w, C.h, 1); /* sharer sees own share */ + ui_local_video(C.frame, C.w, C.h, 1); int key = 0; int force = C.first || net_keyframe_wanted(); C.first = 0; - /* high layer */ + int n = vid_enc_frame(C.venc, C.frame, force, C.enc, 2 * 1024 * 1024, &key); if (n > 0) net_send_video(C.enc, n, key, 1, 1); - /* low layer: 2x2 box downsample, then encode */ if (C.frame_lo) { - int lw = C.w / 2, lh = C.h / 2; - for (int y = 0; y < lh; y++) { - const uint8_t *r0 = C.frame + (size_t)(y * 2) * C.w * 4; - const uint8_t *r1 = r0 + (size_t)C.w * 4; - uint8_t *d = C.frame_lo + (size_t)y * lw * 4; - for (int x = 0; x < lw; x++) { - const uint8_t *a = r0 + x * 8, *b = a + 4; - const uint8_t *c2 = r1 + x * 8, *e = c2 + 4; - for (int k = 0; k < 4; k++) - d[x * 4 + k] = - (uint8_t)((a[k] + b[k] + c2[k] + e[k]) >> 2); - } - } + bgra_half(C.frame, C.w, C.h, C.frame_lo); n = vid_enc_frame(C.venc_lo, C.frame_lo, force, C.enc, - 2 * 1024 * 1024, &key); + 2 * 1024 * 1024, &key); if (n > 0) net_send_video(C.enc, n, key, 1, 0); } } - (void)stream:(SCStream *)stream didStopWithError:(NSError *)error { - if (stream != C.stream) return; + if (stream != C.stream) return; CLOG("capture stopped: %s", - error ? error.localizedDescription.UTF8String : "ok"); + error ? error.localizedDescription.UTF8String : "ok"); C.active = 0; [C.stream release]; C.stream = nil; @@ -167,18 +154,18 @@ int cap_start(void) { if (!C.sink) C.sink = [MilnaCap new]; [SCShareableContent getShareableContentWithCompletionHandler: - ^(SCShareableContent *content, NSError *err) { + ^(SCShareableContent *content, NSError *err) { if (err || !content.displays.count) { CLOG("no shareable content (screen recording permission?): %s", - err ? err.localizedDescription.UTF8String : "no displays"); + err ? err.localizedDescription.UTF8String : "no displays"); C.active = 0; return; } SCDisplay *disp = content.displays.firstObject; SCContentFilter *filter = - [[SCContentFilter alloc] initWithDisplay:disp - excludingWindows:@[]]; + [[SCContentFilter alloc] initWithDisplay:disp + excludingWindows:@[]]; int dw = (int)disp.width, dh = (int)disp.height; if (dw > CAP_MAXW) { dh = dh * CAP_MAXW / dw; dw = CAP_MAXW; } @@ -196,32 +183,32 @@ int cap_start(void) { cfg.channelCount = 2; SCStream *st = [[SCStream alloc] initWithFilter:filter - configuration:cfg - delegate:C.sink]; + configuration:cfg + delegate:C.sink]; [filter release]; [cfg release]; NSError *aerr = nil; [st addStreamOutput:C.sink - type:SCStreamOutputTypeScreen - sampleHandlerQueue:C.q - error:&aerr]; + type:SCStreamOutputTypeScreen + sampleHandlerQueue:C.q + error:&aerr]; if (aerr) { CLOG("addStreamOutput: %s", aerr.localizedDescription.UTF8String); - C.active = 0; return; } + C.active = 0; return; } @try { NSError *auerr = nil; [st addStreamOutput:C.sink - type:SCStreamOutputTypeAudio - sampleHandlerQueue:C.aq - error:&auerr]; + type:SCStreamOutputTypeAudio + sampleHandlerQueue:C.aq + error:&auerr]; if (auerr) { CLOG("audio output (non-fatal): %s", - auerr.localizedDescription.UTF8String); + auerr.localizedDescription.UTF8String); } else { audio_share_start(); C.audio_on = 1; } } @catch (NSException *ex) { CLOG("audio output exception (non-fatal): %s", - ex.reason.UTF8String); + ex.reason.UTF8String); } [st startCaptureWithCompletionHandler:^(NSError *serr) { if (serr) { @@ -246,7 +233,7 @@ void cap_stop(void) { dispatch_semaphore_signal(sem); }]; dispatch_semaphore_wait(sem, - dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC)); + dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC)); [st release]; } if (C.q) dispatch_sync(C.q, ^{}); blob - a1ea5797d101220cedeee4aa00496c59c83762a4 blob + 5ff168c51b18e7c496f7858a61ba52236aa851bf --- src/cap_stub.c +++ src/cap_stub.c @@ -1,4 +1,3 @@ -/* linux doesnt have compatability for screensharing yet, stub is left */ #include "cap.h" int cap_start(void) { return -1; } void cap_stop(void) {} blob - 2234aa90c2625a0e46839d8903cf4676a82011db blob + c4702418e9c97d68b5993a7e3628bd354b1dc99c --- src/cap_win.c +++ src/cap_win.c @@ -20,11 +20,11 @@ static struct { HANDLE th; - HANDLE ath; /* WASAPI loopback audio thread */ + HANDLE ath; volatile int run, active; } C; -#define CAP_AFRAME 480 /* must equal audio FRAME (10ms @ 48k) */ +#define CAP_AFRAME 480 static DWORD WINAPI cap_audio_main(LPVOID arg) { (void)arg; @@ -36,35 +36,35 @@ static DWORD WINAPI cap_audio_main(LPVOID arg) { WAVEFORMATEX *wf = NULL; const CLSID CLSID_MMDeviceEnumerator_ = - {0xbcde0395,0xe52f,0x467c,{0x8e,0x3d,0xc4,0x57,0x92,0x91,0x69,0x2e}}; + {0xbcde0395,0xe52f,0x467c,{0x8e,0x3d,0xc4,0x57,0x92,0x91,0x69,0x2e}}; const IID IID_IMMDeviceEnumerator_ = - {0xa95664d2,0x9614,0x4f35,{0xa7,0x46,0xde,0x8d,0xb6,0x36,0x17,0xe6}}; + {0xa95664d2,0x9614,0x4f35,{0xa7,0x46,0xde,0x8d,0xb6,0x36,0x17,0xe6}}; const IID IID_IAudioClient_ = - {0x1cb9ad4c,0xdbfa,0x4c32,{0xb1,0x78,0xc2,0xf5,0x68,0xa7,0x03,0xb2}}; + {0x1cb9ad4c,0xdbfa,0x4c32,{0xb1,0x78,0xc2,0xf5,0x68,0xa7,0x03,0xb2}}; const IID IID_IAudioCaptureClient_ = - {0xc8adbd64,0xe71e,0x48a0,{0xa4,0xde,0x18,0x5c,0x39,0x5c,0xd3,0x17}}; + {0xc8adbd64,0xe71e,0x48a0,{0xa4,0xde,0x18,0x5c,0x39,0x5c,0xd3,0x17}}; if (CoCreateInstance(&CLSID_MMDeviceEnumerator_, NULL, CLSCTX_ALL, - &IID_IMMDeviceEnumerator_, (void **)&en) != S_OK) goto done; - /* eRender + eConsole = the default speakers; loopback captures its mix */ + &IID_IMMDeviceEnumerator_, (void **)&en) != S_OK) goto done; + if (en->lpVtbl->GetDefaultAudioEndpoint(en, eRender, eConsole, &dev) != S_OK) goto done; if (dev->lpVtbl->Activate(dev, &IID_IAudioClient_, CLSCTX_ALL, NULL, - (void **)&ac) != S_OK) goto done; + (void **)&ac) != S_OK) goto done; if (ac->lpVtbl->GetMixFormat(ac, &wf) != S_OK) goto done; - /* loopback mode: AUDCLNT_STREAMFLAGS_LOOPBACK */ + if (ac->lpVtbl->Initialize(ac, AUDCLNT_SHAREMODE_SHARED, - 0x00020000 /* LOOPBACK */, 10000000, 0, wf, NULL) != S_OK) goto done; + 0x00020000 , 10000000, 0, wf, NULL) != S_OK) goto done; if (ac->lpVtbl->GetService(ac, &IID_IAudioCaptureClient_, - (void **)&cc) != S_OK) goto done; + (void **)&cc) != S_OK) goto done; if (ac->lpVtbl->Start(ac) != S_OK) goto done; int srcrate = wf->nSamplesPerSec; int srcch = wf->nChannels; - int isfloat = (wf->wFormatTag == 3 /* IEEE_FLOAT */) || - (wf->wFormatTag == 0xFFFE /* EXTENSIBLE */ && wf->wBitsPerSample == 32); + int isfloat = (wf->wFormatTag == 3 ) || + (wf->wFormatTag == 0xFFFE && wf->wBitsPerSample == 32); CLOG("loopback: %d Hz, %d ch, %s", srcrate, srcch, - isfloat ? "float" : "int16"); + isfloat ? "float" : "int16"); static int16_t acc[CAP_AFRAME]; int accn = 0; @@ -79,9 +79,8 @@ static DWORD WINAPI cap_audio_main(LPVOID arg) { if (cc->lpVtbl->GetBuffer(cc, &data, &frames, &flags, NULL, NULL) != S_OK) break; for (UINT32 i = 0; i < frames; i++) { - /* downmix this source frame to one mono int16 sample */ int32_t m = 0; - if (flags & 2 /* SILENT */) { + if (flags & 2 ) { m = 0; } else if (isfloat) { const float *fp = (const float *)data + (size_t)i * srcch; @@ -93,7 +92,7 @@ static DWORD WINAPI cap_audio_main(LPVOID arg) { int32_t s = 0; for (int c = 0; c < srcch; c++) s += sp[c]; m = s / srcch; } - /* resample srcrate->48k by simple linear interpolation */ + int16_t cur = (int16_t)m; phase += 1.0; while (phase >= step) { @@ -134,7 +133,7 @@ static DWORD WINAPI cap_main(LPVOID arg) { BITMAPINFO bi = {0}; bi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); bi.bmiHeader.biWidth = sw; - bi.bmiHeader.biHeight = -sh; /* top-down */ + bi.bmiHeader.biHeight = -sh; bi.bmiHeader.biPlanes = 1; bi.bmiHeader.biBitCount = 32; bi.bmiHeader.biCompression = BI_RGB; @@ -145,8 +144,8 @@ static DWORD WINAPI cap_main(LPVOID arg) { uint8_t *frame = malloc((size_t)dw * dh * 4); uint8_t *frame_lo = malloc((size_t)(dw / 2) * (dh / 2) * 4); uint8_t *enc = malloc(2 * 1024 * 1024); - vid_enc *hi = vid_enc_new(dw, dh, CAP_FPS, milna_vbitrate(CAP_KBPS)); - vid_enc *lo = vid_enc_new(dw / 2, dh / 2, CAP_FPS, milna_vbitrate(CAP_KBPS_LO)); + vid_enc *hi = vid_enc_new(dw, dh, CAP_FPS, CAP_KBPS); + vid_enc *lo = vid_enc_new(dw / 2, dh / 2, CAP_FPS, CAP_KBPS_LO); if (!frame || !frame_lo || !enc || !hi || !lo || !shot) { CLOG("capture init failed"); goto out; @@ -161,9 +160,8 @@ static DWORD WINAPI cap_main(LPVOID arg) { CURSORINFO ci = { sizeof(ci) }; if (GetCursorInfo(&ci) && (ci.flags & CURSOR_SHOWING)) DrawIconEx(mdc, ci.ptScreenPos.x, ci.ptScreenPos.y, - ci.hCursor, 0, 0, 0, NULL, DI_NORMAL); + ci.hCursor, 0, 0, 0, NULL, DI_NORMAL); - /* nearest-neighbour scale screen -> frame */ const uint32_t *sp = shot; for (int y = 0; y < dh; y++) { const uint32_t *srow = sp + (size_t)(y * sh / dh) * sw; @@ -172,7 +170,7 @@ static DWORD WINAPI cap_main(LPVOID arg) { drow[x] = srow[x * sw / dw]; } - ui_local_video(frame, dw, dh, 1); /* sharer sees own share */ + ui_local_video(frame, dw, dh, 1); int key = 0; int force = first || net_keyframe_wanted(); @@ -180,18 +178,7 @@ static DWORD WINAPI cap_main(LPVOID arg) { int n = vid_enc_frame(hi, frame, force, enc, 2 * 1024 * 1024, &key); if (n > 0) net_send_video(enc, n, key, 1, 1); - for (int y = 0; y < dh / 2; y++) { /* 2x2 box downsample */ - const uint8_t *r0 = frame + (size_t)(y * 2) * dw * 4; - const uint8_t *r1 = r0 + (size_t)dw * 4; - uint8_t *d = frame_lo + (size_t)y * (dw / 2) * 4; - for (int x = 0; x < dw / 2; x++) { - const uint8_t *a = r0 + x * 8, *b = a + 4; - const uint8_t *c2 = r1 + x * 8, *e2 = c2 + 4; - for (int k = 0; k < 4; k++) - d[x * 4 + k] = - (uint8_t)((a[k] + b[k] + c2[k] + e2[k]) >> 2); - } - } + bgra_half(frame, dw, dh, frame_lo); n = vid_enc_frame(lo, frame_lo, force, enc, 2 * 1024 * 1024, &key); if (n > 0) net_send_video(enc, n, key, 1, 0); blob - f4be7e7ba70fcd8af0ccd731c37145d295c74eb6 blob + a5fe3d5e1a89347a0e543a502809e2ac6f811744 --- src/milna.h +++ src/milna.h @@ -1,34 +1,10 @@ #ifndef MILNA_H #define MILNA_H -#define MILNA_VERSION "1.9" +#define MILNA_VERSION "1.9" #include -typedef struct { - uint32_t *px; - int w, h; -} fb; - -enum { M_NONE, M_MOVE, M_DOWN, M_UP, M_TEXT, M_KEY_DOWN, M_KEY_UP }; -typedef struct { - int kind; - int x, y; - int btn; - int key; - char text[8]; -} in_event; - -void ui_init(void); -void ui_input(const in_event *e); -void ui_frame(fb *f); -void ui_mark_dirty(void); - void ui_local_video(const uint8_t *bgra, int w, int h, int is_screen); void streams_drop_slot(int slot); -int milna_vbitrate(int base_kbps); -int ui_needs_redraw(void); - -void plat_invalidate(void); - #endif blob - 01fbc553eb4f576a0ac8584701a4d1142bdf174b blob + b1ad8927b9ce2b1dda8e0ece4aef21c440719811 --- src/net.c +++ src/net.c @@ -26,6 +26,9 @@ static void net_os_init(void) {} #endif #include #include +#ifndef MSG_DONTWAIT +#define MSG_DONTWAIT 0 +#endif #include #include #include @@ -38,7 +41,7 @@ static void net_os_init(void) {} #define LOG(...) do { fprintf(stderr, "[milna] " __VA_ARGS__); fputc('\n', stderr); } while (0) #define MAX_PEERS 16 -#define VCHUNK 1200 /* udp-safe video chunk payload */ +#define VCHUNK 1200 #define VFRAME_MAX (768 * 1024) #define HDR 8 @@ -54,7 +57,7 @@ static int json_get(const char *json, const char *key, if (*q == '\\' && q[1]) { q++; buf[o++] = (*q == 'n') ? '\n' : (*q == 'r') ? '\r' : - (*q == 't') ? '\t' : *q; + (*q == 't') ? '\t' : *q; } else buf[o++] = *q; q++; } @@ -82,47 +85,76 @@ static void json_escape(const char *in, char *out, siz out[o] = 0; } -/* ---- peer table (slot-indexed; roster + crypto keys) ---- */ static struct peer { int used; char id[32], name[32]; - uint8_t key[crypto_secretbox_KEYBYTES]; /* their sender key */ + uint8_t key[crypto_secretbox_KEYBYTES]; uint8_t nbase[16]; int have_key; - /* video reassembly, per source: [0]=camera [1]=screen */ struct vasm { uint8_t *buf; int seq, len, got, cnt, key; } va[2]; - long last_kfreq; /* rate limit, seconds */ - int muted, deafened; /* their announced a/v state */ + long last_kfreq; + int muted, deafened; } P[MAX_PEERS]; static struct { - int tcp, udp; /* sockets */ - struct sockaddr_in raddr; /* relay addr (for udp) */ + int tcp, udp; + struct sockaddr_in raddr; pthread_t t_tcp, t_udp, t_tick; volatile int run; - pthread_mutex_t mu; /* guards P[], roster ops, tcp writes */ + pthread_mutex_t mu; char self_id[32]; int self_slot; uint8_t token[16]; - int udp_ok; /* hello echoed back */ + int udp_ok; - /* my keys */ uint8_t pk[crypto_box_PUBLICKEYBYTES], sk[crypto_box_SECRETKEYBYTES]; - uint8_t skey[crypto_secretbox_KEYBYTES]; /* my sender key */ + uint8_t skey[crypto_secretbox_KEYBYTES]; uint8_t snbase[16]; - atomic_uint aseq, vseq, saseq; /* per-type packet sequence (saseq = screenshare audio) */ + atomic_uint aseq, vseq, saseq; atomic_int kf_wanted; - atomic_int v_abandons; /* incomplete video frames (loss signal) */ - int want; /* simulcast layer we receive (1 = high) */ - int want_high; /* user/pin asked for high quality */ } N = { .tcp = -1, .udp = -1, .self_slot = -1 }; static net_video_fn v_handler; static void *v_user; -/* ---- helpers ---- */ -static void tcp_line(const char *line) { /* send one control line */ +#define VQ_CAP 8 +static struct vq_item { uint8_t *buf; int len, key, src, slot; } vq[VQ_CAP]; +static int vq_r, vq_n, vq_run; +static pthread_mutex_t vq_mu = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t vq_cv = PTHREAD_COND_INITIALIZER; +static pthread_t t_dec; + +static void *dec_main(void *arg) { + (void)arg; + for (;;) { + pthread_mutex_lock(&vq_mu); + while (vq_run && vq_n == 0) pthread_cond_wait(&vq_cv, &vq_mu); + if (vq_n == 0) { pthread_mutex_unlock(&vq_mu); return NULL; } + struct vq_item it = vq[vq_r]; + vq_r = (vq_r + 1) % VQ_CAP; vq_n--; + pthread_mutex_unlock(&vq_mu); + if (v_handler) v_handler(it.slot, it.buf, it.len, it.key, it.src, v_user); + free(it.buf); + } +} + +static int vq_push(int slot, uint8_t *buf, int len, int key, int src) { + int queued = -1; + pthread_mutex_lock(&vq_mu); + if (vq_run && vq_n < VQ_CAP) { + struct vq_item *it = &vq[(vq_r + vq_n) % VQ_CAP]; + it->buf = buf; it->len = len; it->key = key; + it->src = src; it->slot = slot; + vq_n++; + pthread_cond_signal(&vq_cv); + queued = 0; + } + pthread_mutex_unlock(&vq_mu); + return queued; +} + +static void tcp_line(const char *line) { pthread_mutex_lock(&N.mu); if (N.tcp >= 0) { size_t n = strlen(line); @@ -133,29 +165,28 @@ static void tcp_line(const char *line) { /* send } static void mk_nonce(uint8_t n24[24], const uint8_t nbase[16], - uint32_t seq, uint8_t type) { + uint32_t seq, uint8_t type) { memcpy(n24, nbase, 16); n24[16] = seq; n24[17] = seq >> 8; n24[18] = seq >> 16; n24[19] = seq >> 24; n24[20] = type; n24[21] = n24[22] = n24[23] = 0; } static void udp_send_media(uint8_t type, uint8_t flags, - const uint8_t *pt, int len) { + const uint8_t *pt, int len) { if (!N.udp_ok || N.self_slot < 0) return; uint32_t seq = (type == 2) ? atomic_fetch_add(&N.aseq, 1) - : (type == 4) ? atomic_fetch_add(&N.saseq, 1) - : atomic_fetch_add(&N.vseq, 1); + : (type == 4) ? atomic_fetch_add(&N.saseq, 1) + : atomic_fetch_add(&N.vseq, 1); uint8_t pkt[HDR + VCHUNK + 64 + crypto_secretbox_MACBYTES]; if (HDR + len + (int)crypto_secretbox_MACBYTES > (int)sizeof(pkt)) return; pkt[0] = 1; pkt[1] = type; pkt[2] = (uint8_t)N.self_slot; pkt[3] = flags; pkt[4] = seq; pkt[5] = seq >> 8; pkt[6] = seq >> 16; pkt[7] = seq >> 24; uint8_t nonce[24]; mk_nonce(nonce, N.snbase, seq, type); crypto_secretbox_easy(pkt + HDR, pt, (unsigned long long)len, nonce, N.skey); - sendto(N.udp, (const char *)pkt, HDR + len + crypto_secretbox_MACBYTES, 0, - (struct sockaddr *)&N.raddr, sizeof(N.raddr)); + sendto(N.udp, (const char *)pkt, HDR + len + crypto_secretbox_MACBYTES, + MSG_DONTWAIT, (struct sockaddr *)&N.raddr, sizeof(N.raddr)); } -/* the audio sink: one opus packet per call, from the audio thread */ static void net_sink(const uint8_t *opus, int len, void *user) { (void)user; static int first = 1; @@ -163,15 +194,13 @@ static void net_sink(const uint8_t *opus, int len, voi udp_send_media(2, 0, opus, len); } -/* screenshare/system audio sink: wire type 4 (music stream, separate from - * voice type 2). uses its own sequence so it doesn't disturb voice FEC. */ static void net_share_sink(const uint8_t *opus, int len, void *user) { (void)user; udp_send_media(4, 0, opus, len); } int net_send_video(const uint8_t *frame, int len, int is_key, int is_screen, - int layer) { + int layer) { if (!N.udp_ok) return -1; if (layer < 0 || layer > 3) layer = 0; static uint16_t fseq_[2][4]; @@ -188,7 +217,8 @@ int net_send_video(const uint8_t *frame, int len, int chunk[7] = (uint8_t)((is_screen ? 0x10 : 0) | layer); memcpy(chunk + 8, frame + i * (VCHUNK - 8), n); udp_send_media(3, (uint8_t)((is_key ? 1 : 0) | (layer << 1)), - chunk, n + 8); + chunk, n + 8); + if ((i & 31) == 31) usleep(1000); } return 0; } @@ -218,7 +248,7 @@ int net_peer_state(int slot) { if (slot < 0 || slot >= MAX_PEERS) return 0; pthread_mutex_lock(&N.mu); int s = (P[slot].used ? (P[slot].muted ? 1 : 0) | - (P[slot].deafened ? 2 : 0) : 0); + (P[slot].deafened ? 2 : 0) : 0); pthread_mutex_unlock(&N.mu); return s; } @@ -229,8 +259,8 @@ void net_announce_state(int muted, int deaf) { if (N.tcp < 0) return; char line[128]; snprintf(line, sizeof(line), - "{\"type\":\"state\",\"muted\":%d,\"deaf\":%d}", muted ? 1 : 0, - deaf ? 1 : 0); + "{\"type\":\"state\",\"muted\":%d,\"deaf\":%d}", muted ? 1 : 0, + deaf ? 1 : 0); tcp_line(line); } @@ -242,10 +272,10 @@ static void send_my_key(const char *peer_id, const uin crypto_box_seal(sealed, pt, sizeof(pt), peer_pk); char b64[256]; sodium_bin2base64(b64, sizeof(b64), sealed, sizeof(sealed), - sodium_base64_VARIANT_ORIGINAL); + sodium_base64_VARIANT_ORIGINAL); char line[400]; snprintf(line, sizeof(line), - "{\"type\":\"key\",\"to\":\"%s\",\"blob\":\"%s\"}", peer_id, b64); + "{\"type\":\"key\",\"to\":\"%s\",\"blob\":\"%s\"}", peer_id, b64); tcp_line(line); } @@ -261,7 +291,7 @@ static void peer_add(const char *json) { uint8_t pk[crypto_box_PUBLICKEYBYTES]; size_t pkn; if (sodium_base642bin(pk, sizeof(pk), pub, strlen(pub), NULL, &pkn, - NULL, sodium_base64_VARIANT_ORIGINAL) != 0 || pkn != sizeof(pk)) + NULL, sodium_base64_VARIANT_ORIGINAL) != 0 || pkn != sizeof(pk)) return; pthread_mutex_lock(&N.mu); @@ -273,8 +303,6 @@ static void peer_add(const char *json) { pthread_mutex_unlock(&N.mu); send_my_key(id, pk); - /* tell the newcomer my current mute/deafen state so their roster shows - * the right badge immediately (state msgs are otherwise only on change) */ if (my_muted || my_deaf) net_announce_state(my_muted, my_deaf); LOG("%s in slot %d", p->name, slot); } @@ -287,7 +315,8 @@ static void peer_del(const char *json) { free(P[slot].va[0].buf); free(P[slot].va[1].buf); memset(&P[slot], 0, sizeof(P[slot])); pthread_mutex_unlock(&N.mu); - streams_drop_slot(slot); /* free this peer's video decoders (leak fix) */ + streams_drop_slot(slot); + audio_reset_slot(slot); LOG("slot %d left", slot); } @@ -301,23 +330,19 @@ static void on_ctl(const char *json) { if (json_get(json, "token", tok, sizeof(tok))) { size_t tn; sodium_base642bin(N.token, 16, tok, strlen(tok), NULL, &tn, - NULL, sodium_base64_VARIANT_ORIGINAL); + NULL, sodium_base64_VARIANT_ORIGINAL); } LOG("relay: id=%s", N.self_id); - } else if (!strcmp(type, "joined")) { int slot; if (json_get_int(json, "slot", &slot)) { N.self_slot = slot; LOG("joined, my slot=%d", slot); } - } else if (!strcmp(type, "peer") || !strcmp(type, "peer-joined")) { peer_add(json); - } else if (!strcmp(type, "peer-left")) { peer_del(json); - } else if (!strcmp(type, "state")) { char from[32]; int mu = 0, df = 0; if (json_get(json, "from", from, sizeof(from))) { @@ -330,7 +355,6 @@ static void on_ctl(const char *json) { } pthread_mutex_unlock(&N.mu); } - } else if (!strcmp(type, "key")) { char from[32], blob[256]; if (!json_get(json, "from", from, sizeof(from))) return; @@ -338,8 +362,8 @@ static void on_ctl(const char *json) { uint8_t sealed[48 + crypto_box_SEALBYTES]; size_t sn; if (sodium_base642bin(sealed, sizeof(sealed), blob, strlen(blob), - NULL, &sn, NULL, sodium_base64_VARIANT_ORIGINAL) != 0 || - sn != sizeof(sealed)) return; + NULL, &sn, NULL, sodium_base64_VARIANT_ORIGINAL) != 0 || + sn != sizeof(sealed)) return; uint8_t pt[48]; if (crypto_box_seal_open(pt, sealed, sizeof(sealed), N.pk, N.sk) != 0) return; @@ -353,19 +377,21 @@ static void on_ctl(const char *json) { break; } pthread_mutex_unlock(&N.mu); - } else if (!strcmp(type, "kfreq")) { atomic_store(&N.kf_wanted, 1); } } -static void request_keyframe(struct peer *p) { +static int want_keyframe_locked(struct peer *p, char id_out[32]) { long now = time(NULL); - if (now - p->last_kfreq < 1) return; + if (now - p->last_kfreq < 1) return 0; p->last_kfreq = now; + snprintf(id_out, 32, "%s", p->id); + return 1; +} +static void send_kfreq(const char *id) { char line[80]; - snprintf(line, sizeof(line), - "{\"type\":\"kfreq\",\"to\":\"%s\"}", p->id); + snprintf(line, sizeof(line), "{\"type\":\"kfreq\",\"to\":\"%s\"}", id); tcp_line(line); } @@ -382,27 +408,21 @@ static void on_media(const uint8_t *pkt, int n) { int ptlen = n - HDR - crypto_secretbox_MACBYTES; if (ptlen <= 0 || ptlen > (int)sizeof(pt)) return; if (crypto_secretbox_open_easy(pt, pkt + HDR, n - HDR, nonce, p->key) != 0) - return; /* bad mac / wrong key: drop */ + return; - if (type == 2) { /* audio: one opus packet */ + if (type == 2) { audio_recv_packet(slot, (int)seq, pt, ptlen); - - } else if (type == 4) { /* screenshare/system audio (music) */ - audio_share_recv(slot, pt, ptlen); - - } else if (type == 3 && ptlen > 8) { /* video chunk */ + } else if (type == 4) { + audio_share_recv(slot, (int)seq, pt, ptlen); + } else if (type == 3 && ptlen > 8) { int fseq = pt[0] | pt[1] << 8; int cnt = pt[4] | pt[5] << 8; int key = pt[6]; - int src = (pt[7] >> 4) & 1; /* 0=camera 1=screen */ + int src = (pt[7] >> 4) & 1; int dlen = ptlen - 8; int idx = pt[2] | pt[3] << 8; - /* reassembly buffers (p->va[].buf) are freed by peer_del on the TCP - * thread. hold N.mu across the buffer ops and RE-CHECK p->used after - * locking, so a peer leaving mid-frame can't free the buffer out - * from under this memcpy (was a use-after-free / data race). the - * critical section is just a memcpy, so it stays cheap. */ uint8_t *done_buf = NULL; int done_len = 0, done_key = 0; + char kfid[32]; int want_kf = 0; pthread_mutex_lock(&N.mu); if (!p->used) { pthread_mutex_unlock(&N.mu); return; } struct vasm *a = &p->va[src]; @@ -412,10 +432,8 @@ static void on_media(const uint8_t *pkt, int n) { a->seq = -1; } if (fseq != a->seq) { - if (a->seq >= 0 && a->got < a->cnt) { - atomic_fetch_add(&N.v_abandons, 1); - request_keyframe(p); /* abandoned an incomplete frame */ - } + if (a->seq >= 0 && a->got < a->cnt) + want_kf = want_keyframe_locked(p, kfid); a->seq = fseq; a->len = 0; a->got = 0; a->cnt = cnt; a->key = key; } @@ -427,10 +445,6 @@ static void on_media(const uint8_t *pkt, int n) { if (a->got == a->cnt && v_handler) { static int first = 1; if (first) { LOG("first video frame RECEIVED (slot %d, %d bytes)", slot, a->len); first = 0; } - /* copy the completed frame into a private buffer UNDER the lock, - * so peer_del freeing a->buf can't race the decode. decode the - * copy after unlocking (the decoder can be slow and must not - * hold the peer lock). */ done_buf = malloc((size_t)a->len); if (done_buf) { memcpy(done_buf, a->buf, (size_t)a->len); @@ -438,14 +452,17 @@ static void on_media(const uint8_t *pkt, int n) { } } pthread_mutex_unlock(&N.mu); - if (done_buf) { - v_handler(slot, done_buf, done_len, done_key, src, v_user); + if (want_kf) send_kfreq(kfid); + if (done_buf && vq_push(slot, done_buf, done_len, done_key, src) != 0) { free(done_buf); + pthread_mutex_lock(&N.mu); + int kf = p->used ? want_keyframe_locked(p, kfid) : 0; + pthread_mutex_unlock(&N.mu); + if (kf) send_kfreq(kfid); } } } -/* ---- threads ---- */ static void *tcp_main(void *arg) { (void)arg; char buf[8192]; int fill = 0; @@ -474,7 +491,7 @@ static void *udp_main(void *arg) { long n = sock_read(N.udp, pkt, sizeof(pkt)); if (n <= 0) { if (!N.run) break; continue; } if (n >= HDR && pkt[0] == 1) { - if (pkt[1] == 1) { /* hello echo: udp path works */ + if (pkt[1] == 1) { if (!N.udp_ok) LOG("udp media path up"); N.udp_ok = 1; } else on_media(pkt, (int)n); @@ -488,52 +505,13 @@ static void send_hello(void) { pkt[0] = 1; pkt[1] = 1; memcpy(pkt + HDR, N.token, 16); sendto(N.udp, (const char *)pkt, sizeof(pkt), 0, - (struct sockaddr *)&N.raddr, sizeof(N.raddr)); + (struct sockaddr *)&N.raddr, sizeof(N.raddr)); } -static void kfreq_all(void) { - pthread_mutex_lock(&N.mu); - char ids[MAX_PEERS][32]; int n = 0; - for (int s = 0; s < MAX_PEERS; s++) - if (P[s].used) snprintf(ids[n++], 32, "%s", P[s].id); - pthread_mutex_unlock(&N.mu); - for (int i = 0; i < n; i++) { - char line[80]; - snprintf(line, sizeof(line), - "{\"type\":\"kfreq\",\"to\":\"%s\"}", ids[i]); - tcp_line(line); - } -} - -static void set_layer(int want); -void net_want_high(int hi) { /* ui: request high-quality video (pin/quality) */ - N.want_high = hi; - set_layer(hi ? 1 : 0); -} - -static void set_layer(int want) { - N.want = want; - char line[48]; - snprintf(line, sizeof(line), "{\"type\":\"layer\",\"want\":%d}", want); - tcp_line(line); - kfreq_all(); /* start the new layer on a keyframe asap */ -} - static void *tick_main(void *arg) { (void)arg; - int win[5] = {0}, wi = 0, sec = 0, clean = 0; while (N.run) { - send_hello(); /* bind + keepalive + nat refresh */ - if (++sec >= 15) sec = 0; - win[wi] = atomic_exchange(&N.v_abandons, 0); - wi = (wi + 1) % 5; - int recent = win[0] + win[1] + win[2] + win[3] + win[4]; - clean = win[(wi + 4) % 5] ? 0 : clean + 1; - if (N.want == 1 && recent >= 3) { - set_layer(0); /* loss: drop to low layer (silent) */ - } else if (N.want == 0 && clean >= 30 && N.want_high) { - set_layer(1); /* recovered AND user wants high: climb back */ - } + send_hello(); sleep(1); } return NULL; @@ -565,9 +543,8 @@ int net_join(const char *server, const char *room, con LOG("control connect failed"); sock_close(N.tcp); N.tcp = -1; freeaddrinfo(res); return -3; } - { int one = 1; - setsockopt(N.tcp, IPPROTO_TCP, TCP_NODELAY, - (const char *)&one, sizeof(one)); } + int one = 1; + setsockopt(N.tcp, IPPROTO_TCP, TCP_NODELAY, (const char *)&one, sizeof(one)); memcpy(&N.raddr, res->ai_addr, sizeof(N.raddr)); N.raddr.sin_port = htons((uint16_t)port); freeaddrinfo(res); @@ -576,19 +553,20 @@ int net_join(const char *server, const char *room, con if (N.udp >= 0) { int tos = 0xB8; setsockopt(N.udp, IPPROTO_IP, IP_TOS, - (const char *)&tos, sizeof(tos)); - int rcv = 1 << 20; /* 1 MiB */ + (const char *)&tos, sizeof(tos)); + int rcv = 1 << 20; setsockopt(N.udp, SOL_SOCKET, SO_RCVBUF, - (const char *)&rcv, sizeof(rcv)); + (const char *)&rcv, sizeof(rcv)); + int snd = 1 << 20; + setsockopt(N.udp, SOL_SOCKET, SO_SNDBUF, + (const char *)&snd, sizeof(snd)); } - /* identity + sender key for this session */ crypto_box_keypair(N.pk, N.sk); randombytes_buf(N.skey, sizeof(N.skey)); randombytes_buf(N.snbase, sizeof(N.snbase)); atomic_store(&N.aseq, 0); atomic_store(&N.vseq, 0); atomic_store(&N.saseq, 0); - N.udp_ok = 0; N.self_slot = -1; N.want = 0; N.want_high = 0; /* LOCKED low */ - atomic_store(&N.v_abandons, 0); + N.udp_ok = 0; N.self_slot = -1; memset(P, 0, sizeof(P)); N.run = 1; @@ -596,17 +574,21 @@ int net_join(const char *server, const char *room, con char pub[64]; sodium_bin2base64(pub, sizeof(pub), N.pk, sizeof(N.pk), - sodium_base64_VARIANT_ORIGINAL); + sodium_base64_VARIANT_ORIGINAL); char esc[64]; json_escape(name && name[0] ? name : "anon", esc, sizeof(esc)); char line[300]; snprintf(line, sizeof(line), - "{\"type\":\"join\",\"room\":\"%s\",\"name\":\"%s\",\"pub\":\"%s\"}", - room, esc, pub); + "{\"type\":\"join\",\"room\":\"%s\",\"name\":\"%s\",\"pub\":\"%s\"}", + room, esc, pub); tcp_line(line); for (int i = 0; i < 250 && N.self_slot < 0; i++) usleep(20 * 1000); if (N.self_slot < 0) { LOG("no welcome from relay"); } + pthread_mutex_lock(&vq_mu); + vq_r = 0; vq_n = 0; vq_run = 1; + pthread_mutex_unlock(&vq_mu); + pthread_create(&t_dec, NULL, dec_main, NULL); pthread_create(&N.t_udp, NULL, udp_main, NULL); pthread_create(&N.t_tick, NULL, tick_main, NULL); @@ -627,6 +609,12 @@ void net_leave(void) { pthread_join(N.t_tcp, NULL); pthread_join(N.t_udp, NULL); pthread_join(N.t_tick, NULL); + + pthread_mutex_lock(&vq_mu); + vq_run = 0; + pthread_cond_broadcast(&vq_cv); + pthread_mutex_unlock(&vq_mu); + pthread_join(t_dec, NULL); pthread_mutex_lock(&N.mu); for (int s = 0; s < MAX_PEERS; s++) { free(P[s].va[0].buf); free(P[s].va[1].buf); } memset(P, 0, sizeof(P)); blob - 5c5b6112e0c28e0ee1885d9fbd4388e76fd80b15 blob + 422b982fd353585e849a0cae6fd762065d9d64bf --- src/net.h +++ src/net.h @@ -2,22 +2,20 @@ #define NET_H #include -/* server: "host" or "host:port" (old ws:// urls tolerated). 0 on success. */ int net_join(const char *server, const char *room, const char *name); void net_leave(void); -int net_self_slot(void); /* my media slot, -1 before joined */ +int net_self_slot(void); int net_peers(int slots[], char names[][32], int max); int net_send_video(const uint8_t *frame, int len, int is_key, int is_screen, - int layer); -/* complete received frames, per sender slot + source (network thread!) */ + int layer); + typedef void (*net_video_fn)(int slot, const uint8_t *frame, int len, - int is_key, int is_screen, void *user); + int is_key, int is_screen, void *user); void net_set_video_handler(net_video_fn fn, void *user); int net_keyframe_wanted(void); -void net_want_high(int hi); /* request high-quality video layer */ -int net_peer_state(int slot); /* bit0=muted bit1=deafened */ +int net_peer_state(int slot); void net_announce_state(int muted, int deaf); #endif blob - 858f1aa19f9d4220927f323f4299ec5cb0e04406 (mode 644) blob + /dev/null --- src/plat_mac.m +++ /dev/null @@ -1,196 +0,0 @@ -#import -#import -#import -#include -#include -#include "milna.h" -#include "update.h" -#include -#include "microui.h" - -static fb F; -static CGColorSpaceRef CS; -static NSView *g_view; - -void update_dispatch_bg(void) { - dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ - @autoreleasepool { - update_run_blocking(); - } - dispatch_async(dispatch_get_main_queue(), ^{ - if (g_view) [g_view setNeedsDisplay:YES]; - }); - }); -} - -void plat_invalidate(void) { - NSView *v = g_view; - if (!v) return; - dispatch_async(dispatch_get_main_queue(), ^{ - [v setNeedsDisplay:YES]; - }); -} - -static int mu_key(unsigned short kc) { - switch (kc) { - case 56: case 60: return MU_KEY_SHIFT; - case 59: case 62: return MU_KEY_CTRL; - case 36: case 76: return MU_KEY_RETURN; - case 51: return MU_KEY_BACKSPACE; - default: return 0; - } -} - -@interface MilnaView : NSView -@end - -@implementation MilnaView -- (BOOL)isFlipped { return YES; } -- (BOOL)acceptsFirstResponder { return YES; } - -- (void)ensureBuf { - int w = (int)self.bounds.size.width, h = (int)self.bounds.size.height; - if (w <= 0 || h <= 0) return; - if (w == F.w && h == F.h && F.px) return; - free(F.px); F.w = w; F.h = h; - F.px = malloc((size_t)w * h * 4); -} - -- (void)drawRect:(NSRect)dirty { - (void)dirty; - [self ensureBuf]; - if (!F.px) return; - ui_frame(&F); - CGContextRef ctx = [[NSGraphicsContext currentContext] CGContext]; - - static CGDataProviderRef dp; - static void *dp_ptr; static size_t dp_sz; - size_t sz = (size_t)F.w * F.h * 4; - if (dp_ptr != F.px || dp_sz != sz) { - if (dp) CGDataProviderRelease(dp); - dp = CGDataProviderCreateWithData(NULL, F.px, sz, NULL); - dp_ptr = F.px; dp_sz = sz; - } - CGImageRef img = CGImageCreate(F.w, F.h, 8, 32, (size_t)F.w * 4, CS, - kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Little, - dp, NULL, false, kCGRenderingIntentDefault); - - CGContextSaveGState(ctx); - CGContextTranslateCTM(ctx, 0, F.h); - CGContextScaleCTM(ctx, 1.0, -1.0); - CGContextDrawImage(ctx, CGRectMake(0, 0, F.w, F.h), img); - CGContextRestoreGState(ctx); - - CGImageRelease(img); -} - -- (void)feed:(in_event)e { ui_input(&e); [self setNeedsDisplay:YES]; } - -- (void)feedChar:(unsigned char)c { - if (c >= 32 && c < 127) { - in_event e = {.kind=M_TEXT}; - e.text[0] = (char)c; e.text[1] = 0; - [self feed:e]; - } -} - -- (mu_Vec2)pt:(NSEvent *)ev { - NSPoint p = [self convertPoint:ev.locationInWindow fromView:nil]; - return mu_vec2((int)p.x, (int)p.y); -} -- (void)mouseMoved:(NSEvent *)ev { mu_Vec2 p=[self pt:ev]; [self feed:(in_event){.kind=M_MOVE,.x=p.x,.y=p.y}]; } -- (void)mouseDragged:(NSEvent *)ev { [self mouseMoved:ev]; } -- (void)mouseDown:(NSEvent *)ev { mu_Vec2 p=[self pt:ev]; [self feed:(in_event){.kind=M_DOWN,.x=p.x,.y=p.y,.btn=MU_MOUSE_LEFT}]; } -- (void)mouseUp:(NSEvent *)ev { mu_Vec2 p=[self pt:ev]; [self feed:(in_event){.kind=M_UP,.x=p.x,.y=p.y,.btn=MU_MOUSE_LEFT}]; } -- (void)rightMouseDown:(NSEvent *)ev { mu_Vec2 p=[self pt:ev]; [self feed:(in_event){.kind=M_DOWN,.x=p.x,.y=p.y,.btn=MU_MOUSE_RIGHT}]; } -- (void)rightMouseUp:(NSEvent *)ev { mu_Vec2 p=[self pt:ev]; [self feed:(in_event){.kind=M_UP,.x=p.x,.y=p.y,.btn=MU_MOUSE_RIGHT}]; } - -- (void)keyDown:(NSEvent *)ev { - NSEventModifierFlags mods = ev.modifierFlags; - - /* ---- cmd shortcuts (no menu bar, so handle them here) ---- */ - if (mods & NSEventModifierFlagCommand) { - NSString *k = ev.charactersIgnoringModifiers.lowercaseString; - if ([k isEqualToString:@"q"]) { [NSApp terminate:nil]; return; } - if ([k isEqualToString:@"w"]) { [self.window close]; return; } - if ([k isEqualToString:@"m"]) { [self.window miniaturize:nil]; return; } - if ([k isEqualToString:@"v"]) { - NSString *pb = [[NSPasteboard generalPasteboard] - stringForType:NSPasteboardTypeString]; - if (pb) { - const char *u = pb.UTF8String; - int fed = 0; - for (const char *p = u; *p && fed < 200; p++, fed++) - [self feedChar:(unsigned char)*p]; - } - return; - } - return; - } - - int mk = mu_key(ev.keyCode); - if (mk) { [self feed:(in_event){.kind=M_KEY_DOWN,.key=mk}]; return; } - - const char *s = ev.characters.UTF8String; - if (s && s[0] && !s[1]) [self feedChar:(unsigned char)s[0]]; -} -- (void)keyUp:(NSEvent *)ev { - if (ev.modifierFlags & NSEventModifierFlagCommand) return; - int mk = mu_key(ev.keyCode); - if (mk) [self feed:(in_event){.kind=M_KEY_UP,.key=mk}]; -} -@end - -@interface App : NSObject -@end -@implementation App -- (void)applicationDidFinishLaunching:(NSNotification *)n { - (void)n; - NSRect r = NSMakeRect(0, 0, 400, 280); - NSWindow *win = [[NSWindow alloc] initWithContentRect:r - styleMask:(NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | - NSWindowStyleMaskResizable | NSWindowStyleMaskMiniaturizable) - backing:NSBackingStoreBuffered defer:NO]; - win.title = @"milna"; - win.acceptsMouseMovedEvents = YES; - MilnaView *v = [[MilnaView alloc] initWithFrame:r]; - g_view = v; - win.contentView = v; - [win makeFirstResponder:v]; - [win center]; - [win makeKeyAndOrderFront:nil]; - [NSApp activateIgnoringOtherApps:YES]; - - [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio - completionHandler:^(BOOL granted) { - if (!granted) NSLog(@"[milna] microphone permission denied"); - }]; - - NSTimer *ut = [NSTimer timerWithTimeInterval:2.0 repeats:YES - block:^(NSTimer *t) { - static int kicked, shown; - if (!kicked) { update_check_async(); kicked = 1; } - if (!shown && update_ready()) { - shown = 1; - ui_mark_dirty(); /* force build() to add the banner */ - plat_invalidate(); - [t invalidate]; /* banner is up; stop polling */ - } - }]; - [[NSRunLoop mainRunLoop] addTimer:ut forMode:NSRunLoopCommonModes]; -} -- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)a { (void)a; return YES; } -@end - -int main(void) { - CS = CGColorSpaceCreateDeviceRGB(); - ui_init(); - @autoreleasepool { - [NSApplication sharedApplication]; - [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; - App *d = [App new]; - NSApp.delegate = d; - [NSApp run]; /* event-driven run loop: blocks until events */ - } - return 0; -} blob - /dev/null blob + 4854b14e81bb42fe4ca2cd451a43e16d5ed06a58 (mode 644) --- /dev/null +++ src/ui.cpp @@ -0,0 +1,668 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#define mkdir(p, m) _mkdir(p) +#else +#include +#endif + +extern "C" { +#include "milna.h" +#include "audio.h" +#include "net.h" +#include "vid.h" +#include "cap.h" +#include "cam.h" +#include "update.h" +} + +static struct { + char server[96]; + char room[64]; + char name[32]; + int in_call; + int muted; + int deaf; + int cam_preview; +} A = { "rsap.sh", "lobby", "anon", 0, 0, 0, 0 }; + +static Fl_Double_Window *W; +static Fl_Input *in_server, *in_room, *in_name; +static Fl_Button *b_join, *b_mute, *b_deaf, *b_share, *b_cam; +static Fl_Box *status_box, *banner_box; +static class CallView *call; + +static void conf_dir(char *out, size_t cap) { +#ifdef _WIN32 + const char *a = getenv("APPDATA"); + snprintf(out, cap, "%s/milna", a ? a : "."); +#else + const char *x = getenv("XDG_CONFIG_HOME"); + if (x && x[0]) snprintf(out, cap, "%s/milna", x); + else snprintf(out, cap, "%s/.config/milna", + getenv("HOME") ? getenv("HOME") : "."); +#endif +} + +static void conf_path(char *out, size_t cap) { + char d[480]; + conf_dir(d, sizeof(d)); + snprintf(out, cap, "%s/rc", d); +} + +static void conf_load(void) { + char p[512]; conf_path(p, sizeof(p)); + FILE *f = fopen(p, "r"); + if (!f) return; + char line[256]; + while (fgets(line, sizeof(line), f)) { + char *nl = strchr(line, '\n'); if (nl) *nl = 0; + char *eq = strchr(line, '='); if (!eq) continue; + *eq = 0; + const char *k = line, *v = eq + 1; + if (!strcmp(k, "server")) snprintf(A.server, sizeof(A.server), "%s", v); + else if (!strcmp(k, "room")) snprintf(A.room, sizeof(A.room), "%s", v); + else if (!strcmp(k, "name")) snprintf(A.name, sizeof(A.name), "%s", v); + else if (!strcmp(k, "in")) audio_select_by_name(1, v); + else if (!strcmp(k, "out")) audio_select_by_name(0, v); + } + fclose(f); +} + +static void grab_inputs(void) { + if (!in_server) return; + snprintf(A.server, sizeof(A.server), "%s", in_server->value()); + snprintf(A.room, sizeof(A.room), "%s", in_room->value()); + snprintf(A.name, sizeof(A.name), "%s", in_name->value()); +} + +static void conf_save(void) { + char d[480]; + conf_dir(d, sizeof(d)); + char *sl = strrchr(d, '/'); + if (sl) { + *sl = 0; + mkdir(d, 0755); + *sl = '/'; + } + mkdir(d, 0755); + char p[512]; + conf_path(p, sizeof(p)); + FILE *f = fopen(p, "w"); + if (!f) return; + fprintf(f, "server=%s\nroom=%s\nname=%s\nin=%s\nout=%s\n", + A.server, A.room, A.name, + audio_sel_name(1), audio_sel_name(0)); + fclose(f); +} + +static long long now_ms(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec * 1000LL + ts.tv_nsec / 1000000LL; +} + +enum { VS_MAX = 16, PREVIEW_SLOT = VS_MAX - 1, STREAM_TTL = 3000 }; + +typedef struct { + vid_dec *dec; + uint8_t *px; + int w, h, drop; + long long last, painted, started; +} vstream; +static vstream s_cam[VS_MAX], s_scr[VS_MAX]; +static pthread_mutex_t vs_mu = PTHREAD_MUTEX_INITIALIZER; + +static std::atomic wake_pending; +static std::atomic last_wake; +static void frame_wake(void *); + +static void vs_store(vstream *s, const uint8_t *bgra, int w, int h) { + long long now = now_ms(); + pthread_mutex_lock(&vs_mu); + int was_live = s->px && now - s->last < STREAM_TTL; + if (!s->px || s->w != w || s->h != h) { + free(s->px); + s->px = (uint8_t *)malloc((size_t)w * h * 4); + s->w = w; s->h = h; + } + if (s->px) { + memcpy(s->px, bgra, (size_t)w * h * 4); + s->last = now; + if (!was_live) s->started = now; + } + pthread_mutex_unlock(&vs_mu); + + long long prev = last_wake.load(std::memory_order_relaxed); + if (now - prev >= 50 && last_wake.compare_exchange_strong(prev, now)) + if (!wake_pending.exchange(1)) Fl::awake(frame_wake, NULL); +} + +static void on_video(int slot, const uint8_t *frame, int len, int is_key, + int is_screen, void *user) { + (void)is_key; (void)user; + if (slot < 0 || slot >= VS_MAX) return; + enum { DW = 1920, DH = 1080 }; + static uint8_t *dbuf; + if (!dbuf) dbuf = (uint8_t *)malloc((size_t)DW * DH * 4); + if (!dbuf) return; + vstream *s = is_screen ? &s_scr[slot] : &s_cam[slot]; + pthread_mutex_lock(&vs_mu); + int dropped = s->drop; s->drop = 0; + pthread_mutex_unlock(&vs_mu); + + if (dropped && s->dec) { vid_dec_del(s->dec); s->dec = NULL; } + if (!s->dec) { s->dec = vid_dec_new(); if (!s->dec) return; } + int w, h; + if (vid_dec_frame(s->dec, frame, len, dbuf, DW * DH * 4, &w, &h) != 1) + return; + vs_store(s, dbuf, w, h); +} + +void ui_local_video(const uint8_t *bgra, int w, int h, int is_screen) { + int sl = net_self_slot(); + if (sl < 0) sl = PREVIEW_SLOT; + if (sl >= VS_MAX || !bgra) return; + vs_store(is_screen ? &s_scr[sl] : &s_cam[sl], bgra, w, h); +} + +static void stream_wipe(vstream *s) { + s->drop = 1; + free(s->px); s->px = NULL; + s->w = s->h = 0; + s->last = s->painted = s->started = 0; +} + +void streams_drop_slot(int slot) { + if (slot < 0 || slot >= VS_MAX) return; + pthread_mutex_lock(&vs_mu); + stream_wipe(&s_cam[slot]); + stream_wipe(&s_scr[slot]); + pthread_mutex_unlock(&vs_mu); +} + +static void streams_clear(void) { + pthread_mutex_lock(&vs_mu); + for (int i = 0; i < VS_MAX; i++) { + if (s_cam[i].dec) vid_dec_del(s_cam[i].dec); + if (s_scr[i].dec) vid_dec_del(s_scr[i].dec); + free(s_cam[i].px); free(s_scr[i].px); + memset(&s_cam[i], 0, sizeof(vstream)); + memset(&s_scr[i], 0, sizeof(vstream)); + } + pthread_mutex_unlock(&vs_mu); +} + +struct R { int x, y, w, h; }; +static int inr(R r, int x, int y) { + return x >= r.x && y >= r.y && x < r.x + r.w && y < r.y + r.h; +} +static R bar_of(R t) { R b = { t.x + 8, t.y + t.h - 14, t.w - 16, 6 }; return b; } + +static uint8_t *scratch; +static int scratch_cap; + +static void gray(int g) { fl_color(fl_rgb_color((uchar)g, (uchar)g, (uchar)g)); } + +static void blit_frame(const uint8_t *bgra, int sw, int sh, R r, int bg) { + gray(bg); + int dw = r.w, dh = 0; + if (r.w >= 8 && r.h >= 8 && sw > 0 && sh > 0) { + dh = sh * r.w / sw; + if (dh > r.h) { dh = r.h; dw = sw * r.h / sh; } + if (dw > 3840) dw = 3840; + } + if (dw < 1 || dh < 1) { fl_rectf(r.x, r.y, r.w, r.h); return; } + int ox = r.x + (r.w - dw) / 2, oy = r.y + (r.h - dh) / 2; + if (ox > r.x) fl_rectf(r.x, r.y, ox - r.x, r.h); + if (ox + dw < r.x + r.w) fl_rectf(ox + dw, r.y, r.x + r.w - ox - dw, r.h); + if (oy > r.y) fl_rectf(r.x, r.y, r.w, oy - r.y); + if (oy + dh < r.y + r.h) fl_rectf(r.x, oy + dh, r.w, r.y + r.h - oy - dh); + int need = dw * dh * 3; + if (need > scratch_cap) { + free(scratch); + scratch = (uint8_t *)malloc((size_t)need); + scratch_cap = scratch ? need : 0; + } + if (!scratch) return; + static int xmap[3840]; + for (int i = 0; i < dw; i++) xmap[i] = i * sw / dw; + for (int y = 0; y < dh; y++) { + const uint8_t *srow = bgra + (size_t)(y * sh / dh) * sw * 4; + uint8_t *d = scratch + (size_t)y * dw * 3; + for (int x = 0; x < dw; x++) { + const uint8_t *p = srow + xmap[x] * 4; + d[0] = p[2]; d[1] = p[1]; d[2] = p[0]; + d += 3; + } + } + fl_draw_image(scratch, ox, oy, dw, dh, 3, 0); +} + +static void tile_label(R t, const char *s) { + gray(221); + fl_draw(s, t.x + 4, t.y + 14); +} + +static void tile_status(R t, int muted, int deaf) { + const char *s = deaf ? "deafened" : muted ? "muted" : NULL; + if (!s) return; + fl_color(fl_rgb_color(230, 64, 64)); + fl_draw(s, t.x + t.w - (int)fl_width(s) - 6, t.y + t.h - 6); +} + +static void vol_bar(R t, int gain) { + R b = bar_of(t); + if (b.w < 20) return; + int fw = b.w * gain / 200; + gray(51); fl_rectf(b.x, b.y, b.w, b.h); + gray(221); fl_rectf(b.x, b.y, fw, b.h); + gray(255); fl_rectf(b.x + fw - 1, b.y - 2, 3, b.h + 4); +} + +static void hover_border(R t) { + gray(221); + fl_rect(t.x, t.y, t.w, t.h); + fl_rect(t.x + 1, t.y + 1, t.w - 2, t.h - 2); +} + +class CallView : public Fl_Widget { + R big; int big_slot; int big_on; int big_remote; + R stile[VS_MAX]; int stile_slot[VS_MAX]; int nstile; + R ctile[VS_MAX + 1]; int ctile_slot[VS_MAX + 1]; int nctile; + int mx, my, hov, drag_slot; + R drag_bar; + int last_sig; + + int hit_id(void) { + for (int i = 0; i < nstile; i++) if (inr(stile[i], mx, my)) return 200 + i; + for (int i = 0; i < nctile; i++) if (inr(ctile[i], mx, my)) return 300 + i; + if (big_on && inr(big, mx, my)) return 100; + return -1; + } + + void set_gain_from_x(void) { + int pct = (Fl::event_x() - drag_bar.x) * 200 / (drag_bar.w > 0 ? drag_bar.w : 1); + if (pct < 0) pct = 0; + if (pct > 200) pct = 200; + if (drag_slot >= 100) audio_share_set_gain(drag_slot - 100, pct); + else audio_set_gain(drag_slot, pct); + } + +public: + int pin_slot; + CallView(int X, int Y, int Wd, int Hh) : Fl_Widget(X, Y, Wd, Hh), + big_slot(-1), big_on(0), big_remote(0), nstile(0), nctile(0), + mx(-1), my(-1), hov(-1), drag_slot(-1), last_sig(0), pin_slot(-1) {} + + void draw() FL_OVERRIDE { + long long now = now_ms(); + int slots[VS_MAX]; char names[VS_MAX][32]; int pst[VS_MAX]; + int np = A.in_call ? net_peers(slots, names, VS_MAX) : 0; + int self_sl = A.in_call ? net_self_slot() : -1; + for (int i = 0; i < np; i++) pst[i] = net_peer_state(slots[i]); + fl_font(FL_HELVETICA, 12); + + pthread_mutex_lock(&vs_mu); + int lv[VS_MAX], nlive = 0, freshest = -1; long long best = 0; + for (int i = 0; i < VS_MAX; i++) + if (s_scr[i].px && now - s_scr[i].last < STREAM_TTL) { + lv[nlive++] = i; + if (s_scr[i].started > best) { freshest = i; best = s_scr[i].started; } + } + int pin = freshest; + if (pin_slot >= 0 && s_scr[pin_slot].px && + now - s_scr[pin_slot].last < STREAM_TTL) pin = pin_slot; + + int sig = np * 31 + pin * 7 + nlive * 131 + w() * 3 + h() * 5 + + hov * 13 + A.muted + A.deaf * 2 + A.in_call * 4; + for (int i = 0; i < np; i++) sig = sig * 33 + slots[i] * 5 + pst[i]; + for (int i = 0; i < np + 1; i++) { + int ssl = i == 0 ? (self_sl >= 0 ? self_sl : PREVIEW_SLOT) : slots[i - 1]; + vstream *s = &s_cam[ssl]; + sig = sig * 2 + (s->px && now - s->last < STREAM_TTL ? 1 : 0); + } + int full = (damage() & ~FL_DAMAGE_USER1) != 0; + if (sig != last_sig) { full = 1; last_sig = sig; } + if (full) { gray(29); fl_rectf(x(), y(), w(), h()); } + + big_on = 0; nstile = 0; nctile = 0; + R area = { x(), y(), w(), h() }; + R tiles = area; + if (pin >= 0) { + R sh = { area.x, area.y, area.w, area.h * 3 / 4 }; + tiles.y += sh.h; tiles.h -= sh.h; + vstream *s = &s_scr[pin]; + int hovd = inr(sh, mx, my); + if (full || s->painted != s->last) { + fl_push_clip(sh.x, sh.y, sh.w, sh.h); + blit_frame(s->px, s->w, s->h, sh, 20); + s->painted = s->last; + const char *lab = "share"; + if (pin == self_sl) lab = "your share"; + else for (int i = 0; i < np; i++) + if (slots[i] == pin) { lab = names[i]; break; } + tile_label(sh, lab); + if (hovd) { + hover_border(sh); + if (pin != self_sl) vol_bar(sh, audio_share_get_gain(pin)); + } + fl_pop_clip(); + } + big = sh; big_slot = pin; big_on = 1; + big_remote = pin != self_sl; + } + + int nshare = pin >= 0 ? nlive - 1 : 0; + int n = np + 1 + nshare; + int cols = pin >= 0 ? n : (n <= 1 ? 1 : n <= 4 ? 2 : 3); + if (cols < 1) cols = 1; + int rows = (n + cols - 1) / cols; + int tw = tiles.w / cols, th = tiles.h / rows; + if (tw < 8 || th < 8) { pthread_mutex_unlock(&vs_mu); return; } + + int ti = 0; + for (int i = 0; i < np + 1; i++, ti++) { + R t = { tiles.x + (ti % cols) * tw + 1, + tiles.y + (ti / cols) * th + 1, tw - 2, th - 2 }; + vstream *s; const char *lab; char self[44]; + int rslot = -1, muted, deaf; + if (i == 0) { + int ssl = self_sl >= 0 ? self_sl : PREVIEW_SLOT; + s = &s_cam[ssl]; + snprintf(self, sizeof(self), "%s (you)", A.name); + lab = self; muted = A.muted; deaf = A.deaf; + } else { + rslot = slots[i - 1]; + s = &s_cam[rslot]; + lab = names[i - 1]; + muted = pst[i - 1] & 1; deaf = pst[i - 1] & 2; + } + int live = s->px && now - s->last < STREAM_TTL; + if (full || (live && s->painted != s->last)) { + fl_push_clip(t.x, t.y, t.w, t.h); + if (full && !live) { gray(32); fl_rectf(t.x, t.y, t.w, t.h); } + if (live) { blit_frame(s->px, s->w, s->h, t, 32); s->painted = s->last; } + tile_label(t, lab); + tile_status(t, muted, deaf); + if (rslot >= 0 && inr(t, mx, my)) vol_bar(t, audio_get_gain(rslot)); + fl_pop_clip(); + } + ctile[nctile] = t; ctile_slot[nctile++] = rslot; + } + for (int k = 0; k < nlive && pin >= 0; k++) { + int sl = lv[k]; + if (sl == pin) continue; + R t = { tiles.x + (ti % cols) * tw + 1, + tiles.y + (ti / cols) * th + 1, tw - 2, th - 2 }; + ti++; + vstream *s = &s_scr[sl]; + if (full || s->painted != s->last) { + fl_push_clip(t.x, t.y, t.w, t.h); + blit_frame(s->px, s->w, s->h, t, 20); + s->painted = s->last; + char lab[44] = "share"; + if (sl == self_sl) snprintf(lab, sizeof(lab), "you (share)"); + else for (int i = 0; i < np; i++) + if (slots[i] == sl) { + snprintf(lab, sizeof(lab), "%s (share)", names[i]); + break; + } + tile_label(t, lab); + if (inr(t, mx, my)) hover_border(t); + fl_pop_clip(); + } + stile[nstile] = t; stile_slot[nstile++] = sl; + } + pthread_mutex_unlock(&vs_mu); + } + + int handle(int e) FL_OVERRIDE { + switch (e) { + case FL_ENTER: return 1; + case FL_LEAVE: + mx = my = -1; + if (hov != -1) { hov = -1; redraw(); } + return 1; + case FL_MOVE: { + mx = Fl::event_x(); my = Fl::event_y(); + int h2 = hit_id(); + if (h2 != hov) { hov = h2; redraw(); } + return 1; } + case FL_PUSH: { + if (Fl::event_button() != FL_LEFT_MOUSE) return 1; + mx = Fl::event_x(); my = Fl::event_y(); + for (int i = 0; i < nctile; i++) + if (ctile_slot[i] >= 0 && inr(bar_of(ctile[i]), mx, my)) { + drag_slot = ctile_slot[i]; drag_bar = bar_of(ctile[i]); + set_gain_from_x(); redraw(); return 1; + } + if (big_on && big_remote && inr(bar_of(big), mx, my)) { + drag_slot = big_slot + 100; drag_bar = bar_of(big); + set_gain_from_x(); redraw(); return 1; + } + for (int i = 0; i < nstile; i++) + if (inr(stile[i], mx, my)) { pin_slot = stile_slot[i]; redraw(); return 1; } + if (big_on && inr(big, mx, my)) { pin_slot = -1; redraw(); return 1; } + return 1; } + case FL_DRAG: + if (drag_slot >= 0) { set_gain_from_x(); redraw(); } + return 1; + case FL_RELEASE: + drag_slot = -1; + return 1; + } + return Fl_Widget::handle(e); + } +}; + +static void frame_wake(void *) { + wake_pending.store(0, std::memory_order_relaxed); + if (call) call->damage(FL_DAMAGE_USER1); +} + +static void setlab(Fl_Widget *w, const char *s) { + if (w->label() != s) w->label(s); +} + +static void sync_labels(void) { + setlab(status_box, A.in_call ? "status: in call" : "status: idle"); + setlab(b_join, A.in_call ? "leave" : "join"); + setlab(b_mute, A.muted ? "unmute" : "mute"); + setlab(b_deaf, A.deaf ? "undeafen" : "deafen"); + setlab(b_share, cap_active() ? "stop share" : "share screen"); + setlab(b_cam, cam_active() ? "stop camera" : "camera"); + if (A.in_call) b_share->activate(); else b_share->deactivate(); +} + +static void join_cb(Fl_Widget *, void *) { + if (!A.in_call) { + grab_inputs(); conf_save(); + if (net_join(A.server, A.room, A.name) == 0) { + A.in_call = 1; + + pthread_mutex_lock(&vs_mu); + stream_wipe(&s_cam[PREVIEW_SLOT]); + pthread_mutex_unlock(&vs_mu); + net_set_video_handler(on_video, NULL); + net_announce_state(A.muted, A.deaf); + } + } else { + grab_inputs(); conf_save(); + cap_stop(); + if (!A.cam_preview) cam_stop(); + net_leave(); + A.in_call = 0; + streams_clear(); + call->pin_slot = -1; + } + sync_labels(); call->redraw(); +} + +static void mute_cb(Fl_Widget *, void *) { + A.muted = !A.muted; + if (!A.muted && A.deaf) { A.deaf = 0; audio_set_deafened(0); } + audio_set_muted(A.muted); + if (A.in_call) net_announce_state(A.muted, A.deaf); + sync_labels(); call->redraw(); +} + +static void deaf_cb(Fl_Widget *, void *) { + A.deaf = !A.deaf; + audio_set_deafened(A.deaf); + A.muted = A.deaf ? 1 : 0; + audio_set_muted(A.muted); + if (A.in_call) net_announce_state(A.muted, A.deaf); + sync_labels(); call->redraw(); +} + +static void share_cb(Fl_Widget *, void *) { + if (!cap_active()) { + if (cap_start() != 0) + fprintf(stderr, "[milna] share unavailable on this platform\n"); + } else cap_stop(); + sync_labels(); call->redraw(); +} + +static void cam_cb(Fl_Widget *, void *) { + if (!cam_active()) { + if (cam_start() != 0) + fprintf(stderr, "[milna] camera unavailable on this platform\n"); + else if (!A.in_call) A.cam_preview = 1; + } else { cam_stop(); A.cam_preview = 0; } + sync_labels(); call->redraw(); +} + +struct DevChoice : Fl_Choice { + int cap; + char names[12][64]; + DevChoice(int X, int Y, int Wd, int Hh, const char *L, int c) + : Fl_Choice(X, Y, Wd, Hh, L), cap(c) { callback(pick, NULL); } + void repop(void) { + int n = audio_list(cap, names, 12); + clear(); + + for (int i = 0; i < n; i++) { add("x"); replace(i, names[i]); } + value(-1); + const char *cur = audio_sel_name(cap); + for (int i = 0; i < n; i++) + if (!strcmp(names[i], cur)) { value(i); break; } + } + int handle(int e) FL_OVERRIDE { + if (e == FL_PUSH) repop(); + return Fl_Choice::handle(e); + } + static void pick(Fl_Widget *w, void *) { + DevChoice *d = (DevChoice *)w; + if (d->value() < 0) return; + audio_select(d->cap, d->value()); + grab_inputs(); conf_save(); + } +}; +static DevChoice *dv_in, *dv_out; + +static void tick_cb(void *) { + sync_labels(); + if (call) call->damage(FL_DAMAGE_USER1); + Fl::repeat_timeout(1.0, tick_cb); +} + +static void update_cb(void *) { + static int kicked; + if (!kicked) { kicked = 1; update_check_async(); } + if (update_ready()) { + static char b[96]; + snprintf(b, sizeof(b), "update %s downloaded, quit and reopen to apply", + update_latest_version()); + banner_box->label(b); + return; + } + Fl::repeat_timeout(2.0, update_cb); +} + +static void win_cb(Fl_Widget *, void *) { + if (Fl::event() == FL_SHORTCUT && Fl::event_key() == FL_Escape) return; + if (A.in_call) { cap_stop(); cam_stop(); net_leave(); } + W->hide(); +} + +#ifdef __APPLE__ +extern "C" void update_dispatch_bg(void) { + pthread_t t; + if (pthread_create(&t, NULL, + [](void *) -> void * { update_run_blocking(); return NULL; }, + NULL) == 0) + pthread_detach(t); +} +#endif + +int main(void) { + update_init(MILNA_VERSION); + conf_load(); + Fl::lock(); + FL_NORMAL_SIZE = 12; + Fl::background(29, 29, 29); + Fl::background2(45, 45, 45); + Fl::foreground(221, 221, 221); + + const int CW = 640, CH = 560, TOP = 244; + int half = (CW - 24) / 2; + W = new Fl_Double_Window(CW, CH, "milna"); + + status_box = new Fl_Box(8, 4, 150, 20, "status: idle"); + status_box->align(FL_ALIGN_INSIDE | FL_ALIGN_LEFT); + banner_box = new Fl_Box(158, 4, CW - 158 - 44, 20); + banner_box->align(FL_ALIGN_INSIDE | FL_ALIGN_LEFT); + banner_box->labelcolor(fl_rgb_color(240, 200, 100)); + Fl_Box *ver = new Fl_Box(CW - 44, 4, 36, 20, "v" MILNA_VERSION); + ver->align(FL_ALIGN_INSIDE | FL_ALIGN_RIGHT); + ver->labelcolor(fl_rgb_color(120, 120, 120)); + + in_server = new Fl_Input(70, 28, CW - 78, 24, "server"); + in_room = new Fl_Input(70, 58, CW - 78, 24, "room"); + in_name = new Fl_Input(70, 88, CW - 78, 24, "name"); + in_server->value(A.server); + in_room->value(A.room); + in_name->value(A.name); + + b_join = new Fl_Button(8, 120, CW - 16, 26, "join"); + b_mute = new Fl_Button(8, 152, half, 26, "mute"); + b_deaf = new Fl_Button(16 + half, 152, half, 26, "deafen"); + b_share = new Fl_Button(8, 184, half, 26, "share screen"); + b_cam = new Fl_Button(16 + half, 184, half, 26, "camera"); + b_join->callback(join_cb); + b_mute->callback(mute_cb); + b_deaf->callback(deaf_cb); + b_share->callback(share_cb); + b_cam->callback(cam_cb); + + dv_in = new DevChoice(38, 216, half - 30, 22, "mic", 1); + dv_out = new DevChoice(16 + half + 30, 216, half - 30, 22, "out", 0); + + call = new CallView(0, TOP, CW, CH - TOP); + W->end(); + W->resizable(call); + W->size_range(420, 380); + W->callback(win_cb); + + sync_labels(); + dv_in->repop(); + dv_out->repop(); + + W->show(); + Fl::add_timeout(1.0, tick_cb); + Fl::add_timeout(2.0, update_cb); + return Fl::run(); +} blob - 0910d7c7f705124e44f91d3c3874fdc66226991d (mode 644) blob + /dev/null --- src/plat_win.c +++ /dev/null @@ -1,135 +0,0 @@ -#include -#include -#include -#include "milna.h" -#include "update.h" -#include "microui.h" - -static fb F; -static BITMAPINFO BI; - -static int mu_key(WPARAM vk) { - switch (vk) { - case VK_SHIFT: return MU_KEY_SHIFT; - case VK_CONTROL: return MU_KEY_CTRL; - case VK_RETURN: return MU_KEY_RETURN; - case VK_BACK: return MU_KEY_BACKSPACE; - default: return 0; - } -} - -static void ensure_buf(int w, int h) { - if (w == F.w && h == F.h && F.px) return; - free(F.px); - F.w = w; F.h = h; - F.px = malloc((size_t)w * h * 4); - BI = (BITMAPINFO){0}; - BI.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); - BI.bmiHeader.biWidth = w; - BI.bmiHeader.biHeight = -h; /* negative = top-down, no flip needed */ - BI.bmiHeader.biPlanes = 1; - BI.bmiHeader.biBitCount = 32; - BI.bmiHeader.biCompression = BI_RGB; -} - -static void redraw(HWND hwnd) { InvalidateRect(hwnd, NULL, FALSE); } - -static HWND g_hwnd; -void plat_invalidate(void) { if (g_hwnd) InvalidateRect(g_hwnd, NULL, FALSE); } - -static LRESULT CALLBACK wndproc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { - in_event e = {0}; - switch (msg) { - case WM_SIZE: - ensure_buf(LOWORD(lp), HIWORD(lp)); - redraw(hwnd); - return 0; - case WM_MOUSEMOVE: - e.kind = M_MOVE; e.x = (short)LOWORD(lp); e.y = (short)HIWORD(lp); - ui_input(&e); redraw(hwnd); return 0; - case WM_LBUTTONDOWN: case WM_LBUTTONUP: - e.kind = (msg == WM_LBUTTONDOWN) ? M_DOWN : M_UP; - e.x = (short)LOWORD(lp); e.y = (short)HIWORD(lp); e.btn = MU_MOUSE_LEFT; - ui_input(&e); redraw(hwnd); return 0; - case WM_RBUTTONDOWN: case WM_RBUTTONUP: - e.kind = (msg == WM_RBUTTONDOWN) ? M_DOWN : M_UP; - e.x = (short)LOWORD(lp); e.y = (short)HIWORD(lp); e.btn = MU_MOUSE_RIGHT; - ui_input(&e); redraw(hwnd); return 0; - case WM_TIMER: - if (wp == 1) { - static int kicked, shown; - if (!kicked) { update_check_async(); kicked = 1; } - if (!shown && update_ready()) { - shown = 1; ui_mark_dirty(); redraw(hwnd); - KillTimer(hwnd, 1); - } - } - return 0; - case WM_KEYDOWN: case WM_KEYUP: { - if (msg == WM_KEYDOWN && wp == 'V' && (GetKeyState(VK_CONTROL) & 0x8000)) { - if (OpenClipboard(hwnd)) { /* paste, capped per frame */ - HANDLE h = GetClipboardData(CF_TEXT); - char *t = h ? GlobalLock(h) : NULL; - if (t) { - int fed = 0; - for (const char *q = t; *q && fed < 200; q++, fed++) - if (*q >= 32 && *q < 127) { - in_event pe = {0}; - pe.kind = M_TEXT; pe.text[0] = *q; - ui_input(&pe); - } - GlobalUnlock(h); - } - CloseClipboard(); - redraw(hwnd); - } - return 0; - } - int mk = mu_key(wp); - if (mk) { e.kind = (msg == WM_KEYDOWN) ? M_KEY_DOWN : M_KEY_UP; e.key = mk; - ui_input(&e); redraw(hwnd); } - return 0; - } - case WM_CHAR: - if (wp >= 32 && wp < 127) { - e.kind = M_TEXT; e.text[0] = (char)wp; - ui_input(&e); redraw(hwnd); - } - return 0; - case WM_PAINT: { - PAINTSTRUCT ps; HDC dc = BeginPaint(hwnd, &ps); - ui_frame(&F); - StretchDIBits(dc, 0, 0, F.w, F.h, 0, 0, F.w, F.h, - F.px, &BI, DIB_RGB_COLORS, SRCCOPY); - EndPaint(hwnd, &ps); - return 0; - } - case WM_DESTROY: PostQuitMessage(0); return 0; - } - return DefWindowProc(hwnd, msg, wp, lp); -} - -int main(void) { /* console subsystem: logs stay visible */ - HINSTANCE inst = GetModuleHandle(NULL); - int show = SW_SHOW; - ui_init(); - WNDCLASS wc = {0}; - wc.lpfnWndProc = wndproc; - wc.hInstance = inst; - wc.lpszClassName = "milna"; - wc.hCursor = LoadCursor(NULL, IDC_ARROW); - RegisterClass(&wc); - - HWND hwnd; g_hwnd = hwnd = CreateWindow("milna", "milna", WS_OVERLAPPEDWINDOW, - CW_USEDEFAULT, CW_USEDEFAULT, 640, 560, NULL, NULL, inst, NULL); - ShowWindow(hwnd, show); - SetTimer(hwnd, 1, 2000, NULL); /* one-shot-ish update check kick */ - - MSG msg; - while (GetMessage(&msg, NULL, 0, 0) > 0) { /* BLOCKS — 0% cpu idle */ - TranslateMessage(&msg); - DispatchMessage(&msg); - } - free(F.px); - return 0; -} blob - bd653e4df5c17574db5c6e8b087a55fc080478ca (mode 644) blob + /dev/null --- src/plat_x11.c +++ /dev/null @@ -1,110 +0,0 @@ -#include -#include -#include -#include -#include -#include "milna.h" -#include "microui.h" - -static int mu_key(KeySym k) { - switch (k) { - case XK_Shift_L: case XK_Shift_R: return MU_KEY_SHIFT; - case XK_Control_L: case XK_Control_R: return MU_KEY_CTRL; - case XK_Return: case XK_KP_Enter: return MU_KEY_RETURN; - case XK_BackSpace: return MU_KEY_BACKSPACE; - default: return 0; - } -} - -static Window g_win; -void plat_invalidate(void) { - if (!g_win) return; - Display *d = XOpenDisplay(NULL); - if (!d) return; - XEvent e = {0}; - e.type = Expose; - e.xexpose.window = g_win; - XSendEvent(d, g_win, False, ExposureMask, &e); - XFlush(d); - XCloseDisplay(d); -} - -int main(void) { - Display *dpy = XOpenDisplay(NULL); - if (!dpy) return 1; - int scr = DefaultScreen(dpy); - int w = 400, h = 280; - Window win; g_win = win = XCreateSimpleWindow(dpy, RootWindow(dpy, scr), 0, 0, w, h, 0, - BlackPixel(dpy, scr), BlackPixel(dpy, scr)); - XStoreName(dpy, win, "milna"); - XSelectInput(dpy, win, ExposureMask | KeyPressMask | KeyReleaseMask | - ButtonPressMask | ButtonReleaseMask | PointerMotionMask | - StructureNotifyMask); - Atom wm_del = XInternAtom(dpy, "WM_DELETE_WINDOW", False); - XSetWMProtocols(dpy, win, &wm_del, 1); - XMapWindow(dpy, win); - - Visual *vis = DefaultVisual(dpy, scr); - GC gc = DefaultGC(dpy, scr); - - fb f = { .w = w, .h = h }; - f.px = malloc((size_t)w * h * 4); - XImage *img = XCreateImage(dpy, vis, DefaultDepth(dpy, scr), ZPixmap, 0, - (char *)f.px, w, h, 32, 0); - - ui_init(); - - for (;;) { - XEvent ev; - XNextEvent(dpy, &ev); - in_event ie = {0}; - int quit = 0, resized = 0; - - switch (ev.type) { - case Expose: ie.kind = M_NONE; break; - case ConfigureNotify: - if (ev.xconfigure.width != f.w || ev.xconfigure.height != f.h) { - f.w = ev.xconfigure.width; f.h = ev.xconfigure.height; - img->data = NULL; XDestroyImage(img); - f.px = malloc((size_t)f.w * f.h * 4); - img = XCreateImage(dpy, vis, DefaultDepth(dpy, scr), ZPixmap, 0, - (char *)f.px, f.w, f.h, 32, 0); - resized = 1; - } - break; - case MotionNotify: ie.kind = M_MOVE; ie.x = ev.xmotion.x; ie.y = ev.xmotion.y; break; - case ButtonPress: case ButtonRelease: - ie.kind = (ev.type == ButtonPress) ? M_DOWN : M_UP; - ie.x = ev.xbutton.x; ie.y = ev.xbutton.y; - ie.btn = (ev.xbutton.button == Button1) ? MU_MOUSE_LEFT : - (ev.xbutton.button == Button3) ? MU_MOUSE_RIGHT : MU_MOUSE_MIDDLE; - break; - case KeyPress: case KeyRelease: { - KeySym ks; char buf[8] = {0}; - int n = XLookupString(&ev.xkey, buf, sizeof(buf) - 1, &ks, NULL); - int mk = mu_key(ks); - if (ev.type == KeyPress) { - if (mk) { ie.kind = M_KEY_DOWN; ie.key = mk; } - else if (n > 0 && (unsigned char)buf[0] >= 32) { - ie.kind = M_TEXT; memcpy(ie.text, buf, n); - } - } else if (mk) { ie.kind = M_KEY_UP; ie.key = mk; } - break; - } - case ClientMessage: - if ((Atom)ev.xclient.data.l[0] == wm_del) quit = 1; - break; - } - if (quit) break; - if (ie.kind != M_NONE) ui_input(&ie); - - if (ie.kind != M_NONE || resized || ev.type == Expose || ui_needs_redraw()) { - ui_frame(&f); - XPutImage(dpy, win, gc, img, 0, 0, 0, 0, f.w, f.h); - } - } - - img->data = NULL; XDestroyImage(img); free(f.px); - XCloseDisplay(dpy); - return 0; -} blob - 2e5a2cec883b4892d77017d77f4976df0d2c4481 (mode 644) blob + /dev/null --- src/ui.c +++ /dev/null @@ -1,719 +0,0 @@ -#include -#include -#include -#include -#include -#include "milna.h" -#include "microui.h" -#include "audio.h" -#include "aproc.h" -#include "net.h" -#include "vid.h" -#include "cap.h" -#include "cam.h" -#include "update.h" - -int milna_vbitrate(int base) { return base; } -#include "atlas.inl" /* ATLAS_*, atlas_texture[], atlas[] */ - -#define MAX_LIST 8 /* roster lines shown in the ui */ - -static struct { - mu_Context mu; - char server[96]; - char room[64]; - char name[32]; - int in_call; - int muted; - int deaf; /* deafened: mic off + all others silenced */ - int cam_preview; /* camera on before joining (landing preview) */ - int dirty; - int show_dev; /* devices panel open */ - char devs[2][12][64]; /* [capture][i] cached names */ - int ndev[2]; /* set when state/input changed; drives redraw */ -} A = { .server = "rsap.sh", .room = "lobby", - .name = "anon", .dirty = 1 }; - - -#ifdef _WIN32 -#include -#define mkdir(p, m) _mkdir(p) -#else -#include -#endif - -static void conf_path(char *out, size_t cap) { -#ifdef _WIN32 - const char *a = getenv("APPDATA"); - snprintf(out, cap, "%s/milna/rc", a ? a : "."); -#else - const char *x = getenv("XDG_CONFIG_HOME"); - if (x && x[0]) snprintf(out, cap, "%s/milna/rc", x); - else snprintf(out, cap, "%s/.config/milna/rc", - getenv("HOME") ? getenv("HOME") : "."); -#endif -} - -static void conf_load(void) { - char p[512]; conf_path(p, sizeof(p)); - FILE *f = fopen(p, "r"); - if (!f) { fprintf(stderr, "[milna] no config at %s (first run?)\n", p); return; } - fprintf(stderr, "[milna] config loaded from %s\n", p); - char line[256]; - while (fgets(line, sizeof(line), f)) { - char *nl = strchr(line, '\n'); if (nl) *nl = 0; - char *eq = strchr(line, '='); if (!eq) continue; - *eq = 0; - const char *k = line, *v = eq + 1; - if (!strcmp(k, "server")) snprintf(A.server, sizeof(A.server), "%s", v); - else if (!strcmp(k, "room")) snprintf(A.room, sizeof(A.room), "%s", v); - else if (!strcmp(k, "name")) snprintf(A.name, sizeof(A.name), "%s", v); - else if (!strcmp(k, "in")) audio_select_by_name(1, v); - else if (!strcmp(k, "out")) audio_select_by_name(0, v); - } - fclose(f); -} - -static void conf_save(void) { - char p[512]; conf_path(p, sizeof(p)); - char d[512]; -#ifdef _WIN32 - snprintf(d, sizeof(d), "%s/milna", getenv("APPDATA") ? getenv("APPDATA") : "."); - mkdir(d, 0755); -#else - const char *x = getenv("XDG_CONFIG_HOME"); - if (x && x[0]) snprintf(d, sizeof(d), "%s", x); - else snprintf(d, sizeof(d), "%s/.config", - getenv("HOME") ? getenv("HOME") : "."); - mkdir(d, 0755); - strncat(d, "/milna", sizeof(d) - strlen(d) - 1); - mkdir(d, 0755); -#endif - FILE *f = fopen(p, "w"); - if (!f) { fprintf(stderr, "[milna] cannot write config %s\n", p); return; } - fprintf(f, "server=%s\nroom=%s\nname=%s\nin=%s\nout=%s\n", - A.server, A.room, A.name, - audio_sel_name(1), audio_sel_name(0)); - fclose(f); - fprintf(stderr, "[milna] config saved to %s\n", p); -} - -typedef struct { - vid_dec *dec; - uint8_t *px; /* tight BGRA, w*h*4 */ - int w, h; - long last; /* ms timestamp of the latest frame */ - long painted; /* last frame stamp actually blitted */ - long started; - -} vstream; -#define VS_MAX 16 -#define PREVIEW_SLOT (VS_MAX - 1) /* local self-preview before joining */ -#define STREAM_TTL 3000 /* hide a stream after 3s of silence */ -static vstream s_cam[VS_MAX], s_scr[VS_MAX]; -static pthread_mutex_t vs_mu = PTHREAD_MUTEX_INITIALIZER; - -static struct { - mu_Rect big; int big_live; - mu_Rect tile[VS_MAX]; int tile_slot[VS_MAX]; int ntile; -} share_hits; - -static struct { - mu_Rect bar[VS_MAX]; int slot[VS_MAX]; int n; -} vol_hits; -static int vol_drag_slot = -1; /* slot whose slider is being dragged */ -static int pin_slot = -1; /* -1 = auto (freshest wins) */ -static int mouse_x = -1, mouse_y = -1; - -static int in_rect(mu_Rect r, int x, int y) { - return x >= r.x && y >= r.y && x < r.x + r.w && y < r.y + r.h; -} - -static long now_ms(void) { - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - return ts.tv_sec * 1000L + ts.tv_nsec / 1000000L; -} - -static void vs_store(vstream *s, const uint8_t *bgra, int w, int h); - -static void on_video(int slot, const uint8_t *frame, int len, int is_key, - int is_screen, void *user) { - (void)is_key; (void)user; - if (slot < 0 || slot >= VS_MAX) return; - enum { DW = 1920, DH = 1080 }; - static uint8_t *scratch; /* one net thread: shared is fine */ - if (!scratch) scratch = malloc((size_t)DW * DH * 4); - if (!scratch) return; - vstream *s = is_screen ? &s_scr[slot] : &s_cam[slot]; - if (!s->dec) { s->dec = vid_dec_new(); if (!s->dec) return; } - int w, h; - if (vid_dec_frame(s->dec, frame, len, scratch, DW * DH * 4, &w, &h) != 1) - return; - vs_store(s, scratch, w, h); -} - -/* store one BGRA frame into a stream slot (decoded or local preview) */ -static void vs_store(vstream *s, const uint8_t *bgra, int w, int h) { - long now = now_ms(); - pthread_mutex_lock(&vs_mu); - int was_live = s->px && (now - s->last < STREAM_TTL); - if (!s->px || s->w != w || s->h != h) { - free(s->px); - s->px = malloc((size_t)w * h * 4); - s->w = w; s->h = h; - } - if (s->px) { - memcpy(s->px, bgra, (size_t)w * h * 4); - s->last = now; - if (!was_live) s->started = now; - } - pthread_mutex_unlock(&vs_mu); - static long last_inv; - if (now - last_inv >= 50) { last_inv = now; plat_invalidate(); } -} - -void ui_local_video(const uint8_t *bgra, int w, int h, int is_screen) { - int sl = net_self_slot(); - if (sl < 0) sl = PREVIEW_SLOT; /* pre-join: dedicated preview slot */ - if (sl < 0 || sl >= VS_MAX || !bgra) return; - vs_store(is_screen ? &s_scr[sl] : &s_cam[sl], bgra, w, h); -} - -void streams_drop_slot(int slot) { - if (slot < 0 || slot >= VS_MAX) return; - pthread_mutex_lock(&vs_mu); - if (s_cam[slot].dec) vid_dec_del(s_cam[slot].dec); - free(s_cam[slot].px); - if (s_scr[slot].dec) vid_dec_del(s_scr[slot].dec); - free(s_scr[slot].px); - memset(&s_cam[slot], 0, sizeof(vstream)); - memset(&s_scr[slot], 0, sizeof(vstream)); - pthread_mutex_unlock(&vs_mu); -} - -static void streams_clear(void) { - pthread_mutex_lock(&vs_mu); - for (int i = 0; i < VS_MAX; i++) { - vid_dec_del(s_cam[i].dec); free(s_cam[i].px); - vid_dec_del(s_scr[i].dec); free(s_scr[i].px); - memset(&s_cam[i], 0, sizeof(vstream)); - memset(&s_scr[i], 0, sizeof(vstream)); - } - pthread_mutex_unlock(&vs_mu); -} - - - -static int text_width(mu_Font font, const char *s, int len) { - (void)font; int w = 0; - if (len < 0) len = (int)strlen(s); - for (const char *p = s; *p && len--; p++) { - unsigned char c = (unsigned char)*p; - if (c < 32 || c > 126) continue; /* skip non-ascii (no glyph) */ - w += atlas[ATLAS_FONT + c].w; - } - return w; -} -static int text_height(mu_Font font) { (void)font; return 18; } - -static inline uint32_t blend(uint32_t dst, mu_Color c, unsigned cov) { - /* cov 0..255 modulates c.a; classic over operator, per channel */ - unsigned a = (c.a * cov) / 255; if (!a) return dst; - unsigned dr = (dst >> 16) & 0xff, dg = (dst >> 8) & 0xff, db = dst & 0xff; - unsigned r = (c.r * a + dr * (255 - a)) / 255; - unsigned g = (c.g * a + dg * (255 - a)) / 255; - unsigned b = (c.b * a + db * (255 - a)) / 255; - return 0xff000000u | (r << 16) | (g << 8) | b; -} - -static fb *FB; /* current target during rasterize */ -static mu_Rect CLIP; /* active clip rect */ - -static inline int clip_test(int x, int y) { - return x >= CLIP.x && y >= CLIP.y && - x < CLIP.x + CLIP.w && y < CLIP.y + CLIP.h; -} - -static void draw_rect(mu_Rect r, mu_Color c) { - int x0 = r.x, y0 = r.y, x1 = r.x + r.w, y1 = r.y + r.h; - if (x0 < CLIP.x) x0 = CLIP.x; - if (y0 < CLIP.y) y0 = CLIP.y; - if (x1 > CLIP.x + CLIP.w) x1 = CLIP.x + CLIP.w; - if (y1 > CLIP.y + CLIP.h) y1 = CLIP.y + CLIP.h; - if (x0 < 0) x0 = 0; - if (y0 < 0) y0 = 0; - if (x1 > FB->w) x1 = FB->w; - if (y1 > FB->h) y1 = FB->h; - for (int y = y0; y < y1; y++) { - uint32_t *row = FB->px + (size_t)y * FB->w; - for (int x = x0; x < x1; x++) row[x] = blend(row[x], c, 255); - } -} - -static void draw_atlas(mu_Rect src, int dx, int dy, mu_Color c) { - for (int sy = 0; sy < src.h; sy++) { - int y = dy + sy; if (y < 0 || y >= FB->h) continue; - const unsigned char *srow = atlas_texture + (src.y + sy) * ATLAS_WIDTH + src.x; - uint32_t *drow = FB->px + (size_t)y * FB->w; - for (int sx = 0; sx < src.w; sx++) { - int x = dx + sx; if (x < 0 || x >= FB->w) continue; - if (!clip_test(x, y)) continue; - unsigned cov = srow[sx]; - if (cov) drow[x] = blend(drow[x], c, cov); - } - } -} - -static void draw_text(const char *s, mu_Vec2 pos, mu_Color c) { - int dx = pos.x; - for (const char *p = s; *p; p++) { - unsigned char ch = (unsigned char)*p; - if (ch < 32 || ch > 126) continue; /* no glyph; skip (was OOB) */ - mu_Rect src = atlas[ATLAS_FONT + ch]; - draw_atlas(src, dx, pos.y, c); - dx += src.w; - } -} - -static void draw_icon(int id, mu_Rect r, mu_Color c) { - mu_Rect src = atlas[id]; - int dx = r.x + (r.w - src.w) / 2, dy = r.y + (r.h - src.h) / 2; - draw_atlas(src, dx, dy, c); -} - -static void build(void) { - mu_Context *ctx = &A.mu; - mu_begin(ctx); - mu_Container *win = mu_get_container(ctx, "milna"); - win->rect = mu_rect(0, 0, FB->w, FB->h); - if (mu_begin_window_ex(ctx, "milna", win->rect, - MU_OPT_NOCLOSE | MU_OPT_NORESIZE | MU_OPT_NOTITLE | MU_OPT_NOFRAME)) { - mu_layout_row(ctx, 1, (int[]){ -1 }, 0); - mu_label(ctx, A.in_call ? "status: in call" : "status: idle"); - if (update_ready()) { - char b[80]; - snprintf(b, sizeof(b), - "update %s downloaded - quit & reopen to apply", - update_latest_version()); - mu_layout_row(ctx, 1, (int[]){ -1 }, 0); - mu_label(ctx, b); - } - mu_layout_row(ctx, 2, (int[]){ 80, -1 }, 0); - mu_label(ctx, "server"); - if (mu_textbox(ctx, A.server, sizeof(A.server)) & MU_RES_CHANGE) A.dirty = 1; - mu_label(ctx, "room"); - if (mu_textbox(ctx, A.room, sizeof(A.room)) & MU_RES_CHANGE) A.dirty = 1; - mu_label(ctx, "name"); - if (mu_textbox(ctx, A.name, sizeof(A.name)) & MU_RES_CHANGE) A.dirty = 1; - int half = (FB->w - 3 * ctx->style->spacing) / 2; - mu_layout_row(ctx, 2, (int[]){ half, -1 }, 0); - if (mu_button(ctx, A.in_call ? "leave" : "join")) { - if (!A.in_call) { - conf_save(); - if (net_join(A.server, A.room, A.name) == 0) { - A.in_call = 1; - net_set_video_handler(on_video, NULL); - /* announce current mute/deafen on join */ - net_announce_state(A.muted, A.deaf); - } - } else { - conf_save(); /* fields may have been edited mid-call */ - cap_stop(); - if (!A.cam_preview) cam_stop(); - net_leave(); A.in_call = 0; - streams_clear(); - pin_slot = -1; - } - A.dirty = 1; - } - if (mu_button(ctx, A.show_dev ? "devices: hide" : "devices")) { - A.show_dev = !A.show_dev; - if (A.show_dev) { /* refresh lists on open */ - A.ndev[1] = audio_list(1, A.devs[1], 12); - A.ndev[0] = audio_list(0, A.devs[0], 12); - } - A.dirty = 1; - } - - /* ROW 2: mute + deafen */ - mu_layout_row(ctx, 2, (int[]){ half, -1 }, 0); - if (mu_button(ctx, A.muted ? "unmute" : "mute")) { - A.muted = !A.muted; - if (!A.muted && A.deaf) { A.deaf = 0; audio_set_deafened(0); } - audio_set_muted(A.muted); - if (A.in_call) net_announce_state(A.muted, A.deaf); - A.dirty = 1; - } - if (mu_button(ctx, A.deaf ? "undeafen" : "deafen")) { - A.deaf = !A.deaf; - audio_set_deafened(A.deaf); - /* deafen implies mic muted too (hear nothing, say nothing) */ - if (A.deaf) { A.muted = 1; } - else { A.muted = 0; } - audio_set_muted(A.muted); - if (A.in_call) net_announce_state(A.muted, A.deaf); - A.dirty = 1; - } - - /* ROW 3: share screen + camera */ - mu_layout_row(ctx, 2, (int[]){ half, -1 }, 0); - if (A.in_call) { - if (mu_button(ctx, cap_active() ? "stop share" : "share screen")) { - if (!cap_active()) { - if (cap_start() != 0) - fprintf(stderr, "[milna] share unavailable on this platform\n"); - } else cap_stop(); - A.dirty = 1; - } - } else { - /* pre-join: screenshare disabled, labelled (not in call) */ - if (mu_button(ctx, "screenshare (not in call)")) { - } - } - if (mu_button(ctx, cam_active() ? "stop camera" : "camera")) { - if (!cam_active()) { - if (cam_start() != 0) - fprintf(stderr, "[milna] camera unavailable on this platform\n"); - else if (!A.in_call) A.cam_preview = 1; /* landing preview */ - } else { cam_stop(); A.cam_preview = 0; } - A.dirty = 1; - } - - if (A.show_dev) { - for (int cap = 1; cap >= 0; cap--) { - char hdr[80]; - const char *cur = audio_sel_name(cap); - snprintf(hdr, sizeof(hdr), "%s: %s", - cap ? "input" : "output", cur[0] ? cur : "default"); - mu_layout_row(ctx, 1, (int[]){ -1 }, 0); - mu_label(ctx, hdr); - for (int i = 0; i < A.ndev[cap]; i++) { - mu_push_id(ctx, &cap, sizeof(cap)); - if (mu_button(ctx, A.devs[cap][i])) { - audio_select(cap, i); - conf_save(); - } - mu_pop_id(ctx); - } - } - } - mu_end_window(ctx); - } - mu_end(ctx); -} - -static void rasterize(void) { - uint32_t bg = 0xff1d1d1du; - for (int i = 0; i < FB->w * FB->h; i++) FB->px[i] = bg; - CLIP = mu_rect(0, 0, FB->w, FB->h); - mu_Command *cmd = NULL; - while (mu_next_command(&A.mu, &cmd)) { - switch (cmd->type) { - case MU_COMMAND_RECT: draw_rect(cmd->rect.rect, cmd->rect.color); break; - case MU_COMMAND_TEXT: draw_text(cmd->text.str, cmd->text.pos, cmd->text.color); break; - case MU_COMMAND_ICON: draw_icon(cmd->icon.id, cmd->icon.rect, cmd->icon.color); break; - case MU_COMMAND_CLIP: CLIP = cmd->clip.rect; break; - } - } - { - char ver[32]; - snprintf(ver, sizeof(ver), "v%s", MILNA_VERSION); - int vw = text_width(NULL, ver, (int)strlen(ver)); - CLIP = mu_rect(0, 0, FB->w, FB->h); - draw_text(ver, mu_vec2(FB->w - vw - 8, 6), mu_color(120, 120, 120, 255)); - } -} -void ui_mark_dirty(void) { A.dirty = 1; } - -void ui_init(void) { - update_init(MILNA_VERSION); - mu_init(&A.mu); - A.mu.text_width = text_width; - A.mu.text_height = text_height; - A.mu.style->colors[MU_COLOR_TEXT] = mu_color(0xdd, 0xdd, 0xdd, 0xff); - A.mu.style->colors[MU_COLOR_WINDOWBG] = mu_color(29, 29, 29, 0xff); - conf_load(); -} - -void ui_input(const in_event *e) { - mu_Context *ctx = &A.mu; - if (e->kind == M_MOVE) { mouse_x = e->x; mouse_y = e->y; A.dirty = 1; } - if (e->kind == M_DOWN && e->btn == MU_MOUSE_LEFT && A.in_call) { - int on_slider = 0; - for (int i = 0; i < vol_hits.n; i++) { - if (in_rect(vol_hits.bar[i], e->x, e->y)) { - vol_drag_slot = vol_hits.slot[i]; - int rel = e->x - vol_hits.bar[i].x; - int pct = rel * 200 / vol_hits.bar[i].w; /* 0..200% */ - if (vol_drag_slot >= 100) - audio_share_set_gain(vol_drag_slot - 100, pct); - else audio_set_gain(vol_drag_slot, pct); - on_slider = 1; A.dirty = 1; - break; - } - } - if (!on_slider) { /* not a slider click -> pin logic */ - if (share_hits.big_live && in_rect(share_hits.big, e->x, e->y)) - pin_slot = -1; - for (int i = 0; i < share_hits.ntile; i++) - if (in_rect(share_hits.tile[i], e->x, e->y)) - pin_slot = share_hits.tile_slot[i]; - } - } - if (e->kind == M_MOVE && vol_drag_slot >= 0) { /* dragging */ - for (int i = 0; i < vol_hits.n; i++) - if (vol_hits.slot[i] == vol_drag_slot) { - int rel = e->x - vol_hits.bar[i].x; - int pct = rel * 200 / vol_hits.bar[i].w; - if (pct < 0) pct = 0; - if (pct > 200) pct = 200; - if (vol_drag_slot >= 100) - audio_share_set_gain(vol_drag_slot - 100, pct); - else audio_set_gain(vol_drag_slot, pct); - A.dirty = 1; - break; - } - } - if (e->kind == M_UP) vol_drag_slot = -1; - switch (e->kind) { - case M_MOVE: mu_input_mousemove(ctx, e->x, e->y); break; - case M_DOWN: mu_input_mousedown(ctx, e->x, e->y, e->btn); break; - case M_UP: mu_input_mouseup(ctx, e->x, e->y, e->btn); break; - case M_TEXT: mu_input_text(ctx, e->text); break; - case M_KEY_DOWN: mu_input_keydown(ctx, e->key); break; - case M_KEY_UP: mu_input_keyup(ctx, e->key); break; - } - A.dirty = 1; /* any input may change the ui */ -} - -int ui_needs_redraw(void) { return A.dirty; } - -#define CONTROLS_H 230 -static int controls_h_now = CONTROLS_H; - -static void px_fill(mu_Rect r, uint32_t col) { - int x0 = r.x < 0 ? 0 : r.x, y0 = r.y < 0 ? 0 : r.y; - int x1 = r.x + r.w > FB->w ? FB->w : r.x + r.w; - int y1 = r.y + r.h > FB->h ? FB->h : r.y + r.h; - for (int y = y0; y < y1; y++) { - uint32_t *row = FB->px + (size_t)y * FB->w; - for (int x = x0; x < x1; x++) row[x] = col; - } -} - -static void blit_scaled_bg(const uint8_t *src, int sw, int sh, mu_Rect r, - uint32_t bg) { - static int xmap[3840]; - if (r.w < 8 || r.h < 8 || sw <= 0 || sh <= 0) return; - int dw = r.w, dh = sh * r.w / sw; - if (dh > r.h) { dh = r.h; dw = sw * r.h / sh; } - if (dw > 3840) dw = 3840; - int ox = r.x + (r.w - dw) / 2, oy = r.y + (r.h - dh) / 2; - if (ox > r.x) px_fill(mu_rect(r.x, r.y, ox - r.x, r.h), bg); - if (ox + dw < r.x + r.w) - px_fill(mu_rect(ox + dw, r.y, r.x + r.w - ox - dw, r.h), bg); - if (oy > r.y) px_fill(mu_rect(r.x, r.y, r.w, oy - r.y), bg); - if (oy + dh < r.y + r.h) - px_fill(mu_rect(r.x, oy + dh, r.w, r.y + r.h - oy - dh), bg); - for (int x = 0; x < dw; x++) xmap[x] = x * sw / dw; - for (int y = 0; y < dh; y++) { - int dy = oy + y; - if (dy < 0 || dy >= FB->h) continue; - const uint32_t *srow = (const uint32_t *)src + (size_t)(y * sh / dh) * sw; - uint32_t *dst = FB->px + (size_t)dy * FB->w; - for (int x = 0; x < dw; x++) { - int dx = ox + x; - if (dx >= 0 && dx < FB->w) dst[dx] = srow[xmap[x]]; - } - } -} - -static void hover_border(mu_Rect t) { - if (!in_rect(t, mouse_x, mouse_y)) return; - uint32_t c = 0xffdddddd; - px_fill(mu_rect(t.x, t.y, t.w, 2), c); - px_fill(mu_rect(t.x, t.y + t.h - 2, t.w, 2), c); - px_fill(mu_rect(t.x, t.y, 2, t.h), c); - px_fill(mu_rect(t.x + t.w - 2, t.y, 2, t.h), c); -} - -static void tile_volume_ex(mu_Rect t, int slot, int is_screen) { - if (slot < 0 || slot >= VS_MAX) return; - if (!in_rect(t, mouse_x, mouse_y)) return; /* hover only */ - int pad = 8, bh = 6; - mu_Rect bar = mu_rect(t.x + pad, t.y + t.h - pad - bh, - t.w - 2 * pad, bh); - if (bar.w < 20) return; - int g = is_screen ? audio_share_get_gain(slot) : audio_get_gain(slot); - int fillw = bar.w * g / 200; - px_fill(bar, 0xff333333); /* track */ - px_fill(mu_rect(bar.x, bar.y, fillw, bh), 0xffdddddd); /* level */ - /* knob */ - px_fill(mu_rect(bar.x + fillw - 1, bar.y - 2, 3, bh + 4), 0xffffffff); - if (vol_hits.n < VS_MAX) { - vol_hits.bar[vol_hits.n] = bar; - vol_hits.slot[vol_hits.n] = is_screen ? slot + 100 : slot; - vol_hits.n++; - } -} - -static void tile_volume(mu_Rect t, int slot) { tile_volume_ex(t, slot, 0); } - -static void tile_label(mu_Rect t, const char *name) { - CLIP = t; - draw_text(name, mu_vec2(t.x + 4, t.y + 4), mu_color(221, 221, 221, 255)); - CLIP = mu_rect(0, 0, FB->w, FB->h); -} - -static void tile_status(mu_Rect t, int muted, int deaf) { - const char *s = deaf ? "deafened" : muted ? "muted" : NULL; - if (!s) return; - CLIP = t; - int tw = text_width(NULL, s, (int)strlen(s)); - draw_text(s, mu_vec2(t.x + t.w - tw - 6, t.y + t.h - 18), - mu_color(230, 64, 64, 255)); /* red */ - CLIP = mu_rect(0, 0, FB->w, FB->h); -} - -static void render_call_view(int all) { - long now = now_ms(); - int slots[VS_MAX]; char names[VS_MAX][32]; - int np = A.in_call ? net_peers(slots, names, VS_MAX) : 0; - - mu_Rect area = mu_rect(0, controls_h_now, FB->w, FB->h - controls_h_now); - if (area.h < 40) return; - - pthread_mutex_lock(&vs_mu); - - int self_sl = net_self_slot(); - int live[VS_MAX], nlive = 0, freshest = -1; long best = 0; - for (int i = 0; i < VS_MAX; i++) - if (s_scr[i].px && now - s_scr[i].last < STREAM_TTL) { - live[nlive++] = i; - if (s_scr[i].started > best) { /* newest START pins */ - freshest = i; best = s_scr[i].started; - } - } - int pin = -1; - if (pin_slot >= 0 && s_scr[pin_slot].px && - now - s_scr[pin_slot].last < STREAM_TTL) pin = pin_slot; - else pin = freshest; - - share_hits.big_live = 0; - share_hits.ntile = 0; - vol_hits.n = 0; - - { - int sig = np * 31 + pin * 7 + nlive * 131 + FB->w * 3 + FB->h * 5 - + controls_h_now * 17; - for (int i = 0; i < np; i++) sig = sig * 33 + slots[i]; - static int last_sig; - if (sig != last_sig) { all = 1; last_sig = sig; } - } - - mu_Rect tiles = area; - if (pin >= 0) { - mu_Rect share = mu_rect(area.x, area.y, area.w, area.h * 3 / 4); - tiles = mu_rect(area.x, area.y + share.h, area.w, area.h - share.h); - if (all || s_scr[pin].painted != s_scr[pin].last) { - if (all) px_fill(share, 0xff141414); - blit_scaled_bg(s_scr[pin].px, s_scr[pin].w, s_scr[pin].h, share, 0xff141414); - s_scr[pin].painted = s_scr[pin].last; - if (pin == self_sl) tile_label(share, "your share"); - else for (int i = 0; i < np; i++) - if (slots[i] == pin) { tile_label(share, names[i]); break; } - hover_border(share); - } - share_hits.big = share; - share_hits.big_live = 1; - - if (pin != self_sl) - tile_volume_ex(share, pin, 1); - } - - int nshare = (pin >= 0) ? nlive - 1 : 0; - int n = np + 1 + nshare; - int cols = (pin >= 0) ? n : (n <= 1 ? 1 : n <= 4 ? 2 : 3); - if (cols < 1) cols = 1; - int rows = (n + cols - 1) / cols; - int tw = tiles.w / cols, th = tiles.h / rows; - if (tw < 8 || th < 8) { pthread_mutex_unlock(&vs_mu); return; } - int ti = 0; - for (int i = 0; i < np + 1; i++, ti++) { - mu_Rect t = mu_rect(tiles.x + (ti % cols) * tw + 1, - tiles.y + (ti / cols) * th + 1, tw - 2, th - 2); - vstream *s = NULL; - const char *lab = NULL; - char self[44]; - if (i == 0) { /* self: local camera preview */ - int ssl = self_sl >= 0 ? self_sl : PREVIEW_SLOT; - if (ssl >= 0 && ssl < VS_MAX) s = &s_cam[ssl]; - snprintf(self, sizeof(self), "%s (you)", A.name); - lab = self; - } else { - s = &s_cam[slots[i - 1]]; - lab = names[i - 1]; - } - int live = s && s->px && now - s->last < STREAM_TTL; - int remote_slot = (i == 0) ? -1 : slots[i - 1]; - int hov = in_rect(t, mouse_x, mouse_y); - if (all || (live && s->painted != s->last) || hov) { - if (all || hov) px_fill(t, 0xff202020); - if (live) { blit_scaled_bg(s->px, s->w, s->h, t, 0xff202020); - s->painted = s->last; } - tile_label(t, lab); - if (remote_slot >= 0) tile_volume(t, remote_slot); /* hover vol */ - /* mute/deafen status badge (bottom-right, red) */ - if (i == 0) - tile_status(t, A.muted, A.deaf); /* my own state */ - else { - int st = net_peer_state(remote_slot); - tile_status(t, st & 1, st & 2); - } - } - } - for (int k = 0; k < nlive && pin >= 0; k++) { - int sl = live[k]; - if (sl == pin) continue; - mu_Rect t = mu_rect(tiles.x + (ti % cols) * tw + 1, - tiles.y + (ti / cols) * th + 1, tw - 2, th - 2); - ti++; - if (all || s_scr[sl].painted != s_scr[sl].last) { - blit_scaled_bg(s_scr[sl].px, s_scr[sl].w, s_scr[sl].h, t, 0xff141414); - s_scr[sl].painted = s_scr[sl].last; - char lab[44] = "share"; - if (sl == self_sl) snprintf(lab, sizeof(lab), "you (share)"); - else for (int i = 0; i < np; i++) - if (slots[i] == sl) { - snprintf(lab, sizeof(lab), "%s (share)", names[i]); - break; - } - tile_label(t, lab); - hover_border(t); - } - share_hits.tile[share_hits.ntile] = t; - share_hits.tile_slot[share_hits.ntile] = sl; - share_hits.ntile++; - } - pthread_mutex_unlock(&vs_mu); -} - -void ui_frame(fb *f) { - controls_h_now = CONTROLS_H; - if (A.show_dev) - controls_h_now += 60 + 26 * (A.ndev[0] + A.ndev[1]); - static uint32_t *last_px; - static int last_w, last_h; - int fb_changed = (f->px != last_px || f->w != last_w || f->h != last_h); - FB = f; - if (A.dirty || fb_changed) { - build(); - rasterize(); /* repaints the whole window background */ - render_call_view(1); /* so the call view must repaint fully */ - last_px = f->px; last_w = f->w; last_h = f->h; - A.dirty = 0; - } else { - render_call_view(0); /* only streams with new frames re-blit */ - } -} blob - b46cf5fa3e8cb58fa688e9740ca1303b6e0ac826 blob + b82face40891902ee4ccb66ad5c2f73f15631609 --- src/update.c +++ src/update.c @@ -10,7 +10,7 @@ static int run_argv(char *const argv[]); #ifndef MILNA_PUBKEY #define MILNA_PUBKEY 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, \ - 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 + 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 #endif static const unsigned char SIGN_PUBKEY[32] = { MILNA_PUBKEY }; @@ -28,7 +28,7 @@ static int signify_sig_bytes(const char *path, unsigne char *nl = strpbrk(line, "\r\n"); if (nl) *nl = 0; unsigned char raw[128]; size_t rawlen = 0; if (sodium_base642bin(raw, sizeof(raw), line, strlen(line), - NULL, &rawlen, NULL, sodium_base64_VARIANT_ORIGINAL) != 0) + NULL, &rawlen, NULL, sodium_base64_VARIANT_ORIGINAL) != 0) break; if (rawlen != 2 + 8 + 64) break; memcpy(sig, raw + 10, 64); @@ -55,10 +55,10 @@ static int verify_file(const char *file, const char *s fclose(f); if (rd != (size_t)sz) { free(buf); return -1; } int ok = crypto_sign_verify_detached(sig, buf, (size_t)sz, - SIGN_PUBKEY) == 0; + SIGN_PUBKEY) == 0; free(buf); if (!ok) fprintf(stderr, "[milna] update: SIGNATURE VERIFY FAILED — " - "refusing to install\n"); + "refusing to install\n"); return ok ? 0 : -1; } @@ -68,10 +68,10 @@ static int fetch_and_verify(const char *url, const cha snprintf(sigurl, sizeof(sigurl), "%s.sig", url); snprintf(sigfile, sizeof(sigfile), "%s.sig", localfile); char *sv[] = { (char*)"curl", (char*)"-fsSL", (char*)"--max-time", - (char*)"30", sigurl, (char*)"-o", sigfile, NULL }; + (char*)"30", sigurl, (char*)"-o", sigfile, NULL }; if (run_argv(sv) != 0) { fprintf(stderr, "[milna] update: could not fetch signature %s\n", - sigurl); + sigurl); return -1; } int r = verify_file(localfile, sigfile); @@ -108,8 +108,8 @@ static void *update_run_blocking_thunk(void *a); static char g_self[1024]; static char g_new[1024]; +#ifdef __APPLE__ static int bundle_path(char *out, size_t cap) { -#ifdef __APPLE__ const char *marker = ".app/Contents/MacOS/"; char tmp[1024]; snprintf(tmp, sizeof(tmp), "%s", g_self); @@ -118,10 +118,8 @@ static int bundle_path(char *out, size_t cap) { p[4] = 0; snprintf(out, cap, "%s", tmp); return 1; -#else - (void)out; (void)cap; return 0; -#endif } +#endif static volatile int g_ready; static char g_latest[32]; static char g_localver[32]; @@ -148,7 +146,6 @@ static void self_path(char *out, size_t cap) { #elif defined(__APPLE__) char raw[1024]; uint32_t sz = sizeof(raw); if (_NSGetExecutablePath(raw, &sz) != 0) { out[0] = 0; return; } - if (!realpath(raw, out)) snprintf(out, cap, "%s", raw); #else ssize_t n = readlink("/proc/self/exe", out, cap - 1); @@ -159,7 +156,6 @@ static void self_path(char *out, size_t cap) { #ifdef _WIN32 #include static int run_argv(char *const argv[]) { - intptr_t r = _spawnvp(_P_WAIT, argv[0], (const char *const *)argv); return r == 0 ? 0 : -1; } @@ -180,7 +176,7 @@ static int run_argv(char *const argv[]) { static int fetch_version(char *buf, size_t cap) { char vp[1100]; snprintf(vp, sizeof(vp), "%s.ver", g_self); char *argv[] = { (char*)"curl", (char*)"-fsSL", (char*)"--max-time", - (char*)"8", (char*)VER_URL, (char*)"-o", vp, NULL }; + (char*)"8", (char*)VER_URL, (char*)"-o", vp, NULL }; if (run_argv(argv) != 0) { remove(vp); return -1; } FILE *f = fopen(vp, "r"); if (!f) return -1; @@ -214,7 +210,7 @@ void update_run_blocking(void) { char remote[32]; if (fetch_version(remote, sizeof(remote)) != 0) { fprintf(stderr, "[milna] update: version check failed (no net / " - "url unreachable)\n"); + "url unreachable)\n"); return; } fprintf(stderr, "[milna] update: local=%s remote=%s\n", local, remote); @@ -223,28 +219,25 @@ void update_run_blocking(void) { return; } fprintf(stderr, "[milna] update: downloading %s%s%s ...\n", - BIN_BASE, PLAT, EXE); + BIN_BASE, PLAT, EXE); char url[1200]; #ifdef __APPLE__ char bp[1024]; int in_bundle = bundle_path(bp, sizeof(bp)); if (in_bundle) { - snprintf(url, sizeof(url), "%smacos-app.zip", BIN_BASE); char zip[1100]; snprintf(zip, sizeof(zip), "%s.zip", g_new); char *dz[] = { (char*)"curl", (char*)"-fsSL", (char*)"--max-time", - (char*)"180", url, (char*)"-o", zip, NULL }; + (char*)"180", url, (char*)"-o", zip, NULL }; fprintf(stderr, "[milna] update: bundle mode, fetching %s\n", url); if (run_argv(dz) != 0) { fprintf(stderr, "[milna] update: DOWNLOAD FAILED — is " - "milna-macos-app.zip on the vps?\n"); + "milna-macos-app.zip on the vps?\n"); remove(zip); return; } - char *xz[] = { (char*)"xattr", (char*)"-d", - (char*)"com.apple.quarantine", zip, NULL }; + (char*)"com.apple.quarantine", zip, NULL }; run_argv(xz); - if (fetch_and_verify(url, zip) != 0) { remove(zip); return; } @@ -253,27 +246,25 @@ void update_run_blocking(void) { g_ready = 1; pthread_mutex_unlock(&g_mu); fprintf(stderr, "[milna] update: %s (bundle) downloaded, banner " - "will show\n", remote); + "will show\n", remote); return; } #endif snprintf(url, sizeof(url), "%s%s%s", BIN_BASE, PLAT, EXE); char *dargv[] = { (char*)"curl", (char*)"-fsSL", (char*)"--max-time", - (char*)"120", url, (char*)"-o", g_new, NULL }; + (char*)"120", url, (char*)"-o", g_new, NULL }; if (run_argv(dargv) != 0) { fprintf(stderr, "[milna] update: DOWNLOAD FAILED — is %s " - "uploaded to the vps?\n", url); + "uploaded to the vps?\n", url); remove(g_new); return; } - if (fetch_and_verify(url, g_new) != 0) { remove(g_new); return; } #ifndef _WIN32 chmod(g_new, 0755); - char *xargv[] = { (char*)"xattr", (char*)"-d", - (char*)"com.apple.quarantine", g_new, NULL }; + (char*)"com.apple.quarantine", g_new, NULL }; run_argv(xargv); #endif pthread_mutex_lock(&g_mu); @@ -291,7 +282,6 @@ void update_init(const char *local_version) { fprintf(stderr, "[milna] update_init: self=%s\n", g_self); #ifdef __APPLE__ - { char bp[1024]; char zip[1100]; snprintf(zip, sizeof(zip), "%s.zip", g_new); @@ -299,31 +289,28 @@ void update_init(const char *local_version) { if (z && bundle_path(bp, sizeof(bp))) { fclose(z); fprintf(stderr, "[milna] update: staged bundle zip found, " - "launching swap helper for %s\n", bp); - + "launching swap helper for %s\n", bp); char hp[] = "/tmp/milna_update.XXXXXX"; int hfd = mkstemp(hp); FILE *h = (hfd >= 0) ? fdopen(hfd, "w") : NULL; if (h) { fchmod(hfd, 0700); - fprintf(h, - "#!/bin/sh\n" - "pid=\"$1\"; app=\"$2\"; zip=\"$3\"\n" - "for i in $(seq 1 50); do kill -0 \"$pid\" 2>/dev/null || break; sleep 0.1; done\n" - "tmp=$(mktemp -d /tmp/milna.XXXXXX) || exit 1\n" - "if ! /usr/bin/unzip -oq \"$zip\" -d \"$tmp\"; then rm -rf \"$tmp\"; exit 1; fi\n" - "newapp=$(find \"$tmp\" -maxdepth 2 -name '*.app' -type d | head -1)\n" - "[ -n \"$newapp\" ] || { rm -rf \"$tmp\"; exit 1; }\n" - "/usr/bin/xattr -cr \"$newapp\" 2>/dev/null\n" - "rm -rf \"$app.old\"\n" - "mv \"$app\" \"$app.old\" 2>/dev/null\n" - "if ! mv \"$newapp\" \"$app\"; then mv \"$app.old\" \"$app\"; rm -rf \"$tmp\"; exit 1; fi\n" - "/usr/bin/codesign --force --deep --sign - \"$app\" 2>/dev/null\n" - "rm -rf \"$app.old\" \"$tmp\" \"$zip\"\n" - "/usr/bin/open \"$app\"\n"); + "#!/bin/sh\n" + "pid=\"$1\"; app=\"$2\"; zip=\"$3\"\n" + "for i in $(seq 1 50); do kill -0 \"$pid\" 2>/dev/null || break; sleep 0.1; done\n" + "tmp=$(mktemp -d /tmp/milna.XXXXXX) || exit 1\n" + "if ! /usr/bin/unzip -oq \"$zip\" -d \"$tmp\"; then rm -rf \"$tmp\"; exit 1; fi\n" + "newapp=$(find \"$tmp\" -maxdepth 2 -name '*.app' -type d | head -1)\n" + "[ -n \"$newapp\" ] || { rm -rf \"$tmp\"; exit 1; }\n" + "/usr/bin/xattr -cr \"$newapp\" 2>/dev/null\n" + "rm -rf \"$app.old\"\n" + "mv \"$app\" \"$app.old\" 2>/dev/null\n" + "if ! mv \"$newapp\" \"$app\"; then mv \"$app.old\" \"$app\"; rm -rf \"$tmp\"; exit 1; fi\n" + "/usr/bin/codesign --force --deep --sign - \"$app\" 2>/dev/null\n" + "rm -rf \"$app.old\" \"$tmp\" \"$zip\"\n" + "/usr/bin/open \"$app\"\n"); fclose(h); - char pidbuf[16]; snprintf(pidbuf, sizeof(pidbuf), "%d", (int)getpid()); char *hv[] = { (char*)"/bin/sh", hp, pidbuf, bp, zip, NULL }; @@ -334,7 +321,7 @@ void update_init(const char *local_version) { _exit(127); } fprintf(stderr, "[milna] update: helper spawned, quitting " - "for bundle swap\n"); + "for bundle swap\n"); _exit(0); } } else if (z) fclose(z); @@ -343,7 +330,7 @@ void update_init(const char *local_version) { FILE *t = fopen(g_new, "rb"); fprintf(stderr, "[milna] update_init: pending .new %s\n", - t ? "FOUND — promoting" : "none"); + t ? "FOUND — promoting" : "none"); if (t) { fclose(t); #ifdef _WIN32 @@ -352,38 +339,32 @@ void update_init(const char *local_version) { MoveFileA(g_self, old); if (MoveFileA(g_new, g_self) == 0) MoveFileA(old, g_self); - else - - ; #else if (rename(g_new, g_self) != 0) { fprintf(stderr, "[milna] update: PROMOTE FAILED (%s) — is %s " - "in a writable location (not /Applications)?\n", - strerror(errno), g_self); + "in a writable location (not /Applications)?\n", + strerror(errno), g_self); } else { - fprintf(stderr, "[milna] update: promoted, relaunching\n"); #ifdef __APPLE__ - { char bundle[1024]; snprintf(bundle, sizeof(bundle), "%s", g_self); char *p = strstr(bundle, ".app/Contents/MacOS/"); if (p) { p[4] = 0; - char *cs[] = { (char*)"codesign", (char*)"--force", - (char*)"--sign", (char*)"-", bundle, NULL }; + (char*)"--sign", (char*)"-", bundle, NULL }; int rc = run_argv(cs); fprintf(stderr, "[milna] update: re-signed bundle " - "(%s, rc=%d)\n", bundle, rc); + "(%s, rc=%d)\n", bundle, rc); } } #endif char *av[] = { g_self, NULL }; execv(g_self, av); fprintf(stderr, "[milna] update: re-exec failed (%s)\n", - strerror(errno)); + strerror(errno)); } #endif } @@ -392,7 +373,6 @@ void update_init(const char *local_version) { } #ifdef __APPLE__ - void update_dispatch_bg(void); #endif @@ -406,13 +386,12 @@ void update_check_async(void) { #else pthread_t th; if (pthread_create(&th, NULL, - (void *(*)(void *))(void *)update_run_blocking_thunk, NULL) == 0) + (void *(*)(void *))(void *)update_run_blocking_thunk, NULL) == 0) pthread_detach(th); #endif } #ifndef __APPLE__ - static void *update_run_blocking_thunk(void *a) { (void)a; update_run_blocking(); return NULL; } blob - 76573d1e8ea0472ddfd2f5e5d3193f61d3810e33 blob + ae03318d7a8d1c8911fefc54e607d63a3e0b03e7 --- src/update.h +++ src/update.h @@ -1,9 +1,9 @@ #ifndef UPDATE_H #define UPDATE_H #include -void update_init(const char *local_version); -void update_check_async(void); -void update_run_blocking(void); -int update_ready(void); -const char *update_latest_version(void); +void update_init(const char *local_version); +void update_check_async(void); +void update_run_blocking(void); +int update_ready(void); +const char *update_latest_version(void); #endif blob - f909cb9a60ee68b9734e30f6584f5dd8102a65bd blob + e94fe83bb0a533ffb934c0c911a96165d27559fc --- src/vid.c +++ src/vid.c @@ -8,7 +8,7 @@ struct vid_enc { vpx_codec_ctx_t ctx; - vpx_image_t img; /* I420 staging */ + vpx_image_t img; int w, h, fps; int64_t pts; }; @@ -21,7 +21,7 @@ static void bgra_to_i420(const uint8_t *s, int w, int uint8_t *U = im->planes[VPX_PLANE_U]; uint8_t *Vp = im->planes[VPX_PLANE_V]; int sy = im->stride[VPX_PLANE_Y], su = im->stride[VPX_PLANE_U], - sv = im->stride[VPX_PLANE_V]; + sv = im->stride[VPX_PLANE_V]; for (int y = 0; y < h; y++) { const uint8_t *p = s + (size_t)y * w * 4; for (int x = 0; x < w; x++) { @@ -32,7 +32,7 @@ static void bgra_to_i420(const uint8_t *s, int w, int for (int y = 0; y < h / 2; y++) { const uint8_t *p = s + (size_t)(y * 2) * w * 4; for (int x = 0; x < w / 2; x++) { - int b = p[0], g = p[1], r = p[2]; /* sample top-left of 2x2 */ + int b = p[0], g = p[1], r = p[2]; U[y * su + x] = (uint8_t)(((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128); Vp[y * sv + x] = (uint8_t)(((112 * r - 94 * g - 18 * b + 128) >> 8) + 128); p += 8; @@ -44,7 +44,7 @@ static void i420_to_bgra(const vpx_image_t *im, uint8_ const uint8_t *U = im->planes[VPX_PLANE_U]; const uint8_t *Vp = im->planes[VPX_PLANE_V]; int sy = im->stride[VPX_PLANE_Y], su = im->stride[VPX_PLANE_U], - sv = im->stride[VPX_PLANE_V]; + sv = im->stride[VPX_PLANE_V]; for (int y = 0; y < h; y++) { uint8_t *p = d + (size_t)y * w * 4; for (int x = 0; x < w; x++) { @@ -63,9 +63,8 @@ static void i420_to_bgra(const vpx_image_t *im, uint8_ } } -/* ---- encoder ---- */ vid_enc *vid_enc_new(int w, int h, int fps, int kbps) { - w &= ~1; h &= ~1; /* I420 needs even dims */ + w &= ~1; h &= ~1; vid_enc *e = calloc(1, sizeof(*e)); if (!e) return NULL; vpx_codec_enc_cfg_t cfg; @@ -75,13 +74,13 @@ vid_enc *vid_enc_new(int w, int h, int fps, int kbps) cfg.rc_target_bitrate = kbps; cfg.rc_end_usage = VPX_CBR; cfg.g_pass = VPX_RC_ONE_PASS; - cfg.g_lag_in_frames = 0; /* realtime: no lookahead */ + cfg.g_lag_in_frames = 0; cfg.kf_mode = VPX_KF_AUTO; - cfg.kf_max_dist = fps * 5; /* keyframe at least every 5s */ + cfg.kf_max_dist = fps * 5; cfg.g_threads = 4; cfg.g_error_resilient = VPX_ERROR_RESILIENT_DEFAULT; if (vpx_codec_enc_init(&e->ctx, vpx_codec_vp8_cx(), &cfg, 0)) { free(e); return NULL; } - vpx_codec_control(&e->ctx, VP8E_SET_CPUUSED, 8); /* fastest realtime */ + vpx_codec_control(&e->ctx, VP8E_SET_CPUUSED, 8); if (!vpx_img_alloc(&e->img, VPX_IMG_FMT_I420, w, h, 16)) { vpx_codec_destroy(&e->ctx); free(e); return NULL; } @@ -90,7 +89,7 @@ vid_enc *vid_enc_new(int w, int h, int fps, int kbps) } int vid_enc_frame(vid_enc *e, const uint8_t *bgra, int force_key, - uint8_t *out, int cap, int *is_key) { + uint8_t *out, int cap, int *is_key) { if (!e) return -1; bgra_to_i420(bgra, e->w, e->h, &e->img); vpx_enc_frame_flags_t fl = force_key ? VPX_EFLAG_FORCE_KF : 0; @@ -126,14 +125,14 @@ vid_dec *vid_dec_new(void) { } int vid_dec_frame(vid_dec *d, const uint8_t *data, int len, - uint8_t *out, int out_cap, int *w, int *h) { + uint8_t *out, int out_cap, int *w, int *h) { if (!d) return -1; if (vpx_codec_decode(&d->ctx, data, (unsigned)len, NULL, 0)) return -2; vpx_codec_iter_t it = NULL; vpx_image_t *im = vpx_codec_get_frame(&d->ctx, &it); if (!im) return 0; int iw = (int)im->d_w, ih = (int)im->d_h; - /* capacity check BEFORE any write: oversized stream cannot overflow */ + if (iw > VID_MAX_W || ih > VID_MAX_H) return -3; if ((int64_t)iw * ih * 4 > out_cap) return -3; i420_to_bgra(im, out, iw, ih); blob - 6dbe02c1700354922009422ef961a5fb54c0e88c blob + ee069167bbcd4aca6b24b3ada12d2d3637b2a31d --- src/vid.h +++ src/vid.h @@ -1,5 +1,6 @@ #ifndef VID_H #define VID_H +#include #include #define VID_MAX_W 3840 @@ -10,12 +11,29 @@ typedef struct vid_dec vid_dec; vid_enc *vid_enc_new(int w, int h, int fps, int kbps); int vid_enc_frame(vid_enc *e, const uint8_t *bgra, int force_key, - uint8_t *out, int cap, int *is_key); + uint8_t *out, int cap, int *is_key); void vid_enc_del(vid_enc *e); vid_dec *vid_dec_new(void); int vid_dec_frame(vid_dec *d, const uint8_t *data, int len, - uint8_t *out, int out_cap, int *w, int *h); + uint8_t *out, int out_cap, int *w, int *h); void vid_dec_del(vid_dec *d); +void vid_nv12_to_bgra(const uint8_t *Y, int ystride, const uint8_t *UV, + int uvstride, int w, int h, uint8_t *dst); + +static inline void bgra_half(const uint8_t *src, int w, int h, uint8_t *dst) { + for (int y = 0; y < h / 2; y++) { + const uint8_t *r0 = src + (size_t)(y * 2) * w * 4; + const uint8_t *r1 = r0 + (size_t)w * 4; + uint8_t *d = dst + (size_t)y * (w / 2) * 4; + for (int x = 0; x < w / 2; x++) { + const uint8_t *a = r0 + x * 8, *b = a + 4; + const uint8_t *c = r1 + x * 8, *e = c + 4; + for (int k = 0; k < 4; k++) + d[x * 4 + k] = (uint8_t)((a[k] + b[k] + c[k] + e[k]) >> 2); + } + } +} + #endif blob - 76e39dc4829367acea5670bdc4380121bc33da12 blob + 5df5e82c10d33afecf23b0bd23cb7bf83cd3bdf1 --- src/vid_mac.m +++ src/vid_mac.m @@ -13,7 +13,7 @@ struct vid_enc { VTCompressionSessionRef sess; int w, h, fps; int64_t pts; - /* per-call output staging, filled by the compression callback */ + uint8_t *out; int out_cap; int out_len; int out_key; }; @@ -27,7 +27,7 @@ static void put_nalu(struct vid_enc *e, const uint8_t } static void enc_cb(void *refcon, void *src, OSStatus status, - VTEncodeInfoFlags flags, CMSampleBufferRef sb) { + VTEncodeInfoFlags flags, CMSampleBufferRef sb) { (void)src; (void)flags; struct vid_enc *e = refcon; if (status != noErr || !sb || !CMSampleBufferDataIsReady(sb)) return; @@ -40,14 +40,14 @@ static void enc_cb(void *refcon, void *src, OSStatus s } else key = 1; e->out_key = key; - if (key) { /* SPS+PPS in-band on keyframes */ + if (key) { CMFormatDescriptionRef fd = CMSampleBufferGetFormatDescription(sb); const uint8_t *ps; size_t pn; size_t cnt = 0; CMVideoFormatDescriptionGetH264ParameterSetAtIndex(fd, 0, NULL, NULL, - &cnt, NULL); + &cnt, NULL); for (size_t i = 0; i < cnt; i++) if (CMVideoFormatDescriptionGetH264ParameterSetAtIndex( - fd, i, &ps, &pn, NULL, NULL) == noErr) + fd, i, &ps, &pn, NULL, NULL) == noErr) put_nalu(e, ps, pn); } @@ -56,9 +56,9 @@ static void enc_cb(void *refcon, void *src, OSStatus s if (CMBlockBufferGetDataPointer(bb, 0, NULL, &total, &data) != noErr) return; size_t off = 0; - while (off + 4 <= total) { /* AVCC -> annex-b */ + while (off + 4 <= total) { uint32_t n = ((uint8_t)data[off] << 24) | ((uint8_t)data[off+1] << 16) - | ((uint8_t)data[off+2] << 8) | (uint8_t)data[off+3]; + | ((uint8_t)data[off+2] << 8) | (uint8_t)data[off+3]; off += 4; if (off + n > total) break; put_nalu(e, (const uint8_t *)data + off, n); @@ -72,14 +72,14 @@ vid_enc *vid_enc_new(int w, int h, int fps, int kbps) if (!e) return NULL; CFMutableDictionaryRef src = CFDictionaryCreateMutable(NULL, 2, - &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); int fmt = kCVPixelFormatType_32BGRA; CFNumberRef nfmt = CFNumberCreate(NULL, kCFNumberIntType, &fmt); CFDictionarySetValue(src, kCVPixelBufferPixelFormatTypeKey, nfmt); CFRelease(nfmt); OSStatus st = VTCompressionSessionCreate(NULL, w, h, - kCMVideoCodecType_H264, NULL, src, NULL, enc_cb, e, &e->sess); + kCMVideoCodecType_H264, NULL, src, NULL, enc_cb, e, &e->sess); CFRelease(src); if (st != noErr) { VLOG("VTCompressionSessionCreate: %d", (int)st); @@ -90,7 +90,7 @@ vid_enc *vid_enc_new(int w, int h, int fps, int kbps) SETP(kVTCompressionPropertyKey_RealTime, kCFBooleanTrue); SETP(kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanFalse); SETP(kVTCompressionPropertyKey_ProfileLevel, - kVTProfileLevel_H264_High_AutoLevel); + kVTProfileLevel_H264_High_AutoLevel); int br = kbps * 1000; CFNumberRef nbr = CFNumberCreate(NULL, kCFNumberIntType, &br); SETP(kVTCompressionPropertyKey_AverageBitRate, nbr); CFRelease(nbr); @@ -115,7 +115,7 @@ vid_enc *vid_enc_new(int w, int h, int fps, int kbps) } int vid_enc_frame(vid_enc *e, const uint8_t *bgra, int force_key, - uint8_t *out, int cap, int *is_key) { + uint8_t *out, int cap, int *is_key) { if (!e) return -1; CVPixelBufferPoolRef pool = VTCompressionSessionGetPixelBufferPool(e->sess); @@ -136,10 +136,10 @@ int vid_enc_frame(vid_enc *e, const uint8_t *bgra, int const void *k[1] = { kVTEncodeFrameOptionKey_ForceKeyFrame }; const void *v[1] = { kCFBooleanTrue }; props = CFDictionaryCreate(NULL, k, v, 1, - &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); } OSStatus st = VTCompressionSessionEncodeFrame(e->sess, pb, - CMTimeMake(e->pts++, e->fps), kCMTimeInvalid, props, NULL, NULL); + CMTimeMake(e->pts++, e->fps), kCMTimeInvalid, props, NULL, NULL); if (props) CFRelease(props); CVPixelBufferRelease(pb); if (st != noErr) return -3; @@ -165,8 +165,8 @@ struct vid_dec { }; static void dec_cb(void *refcon, void *src, OSStatus status, - VTDecodeInfoFlags flags, CVImageBufferRef img, - CMTime pts, CMTime dur) { + VTDecodeInfoFlags flags, CVImageBufferRef img, + CMTime pts, CMTime dur) { (void)src; (void)flags; (void)pts; (void)dur; struct vid_dec *d = refcon; if (status != noErr || !img) return; @@ -192,11 +192,11 @@ static int dec_make_session(struct vid_dec *d) { const uint8_t *ps[2] = { d->sps, d->pps }; size_t pn[2] = { d->sps_n, d->pps_n }; if (CMVideoFormatDescriptionCreateFromH264ParameterSets(NULL, 2, ps, pn, - 4, &d->fd) != noErr) + 4, &d->fd) != noErr) return -1; CFMutableDictionaryRef attrs = CFDictionaryCreateMutable(NULL, 2, - &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); int fmt = kCVPixelFormatType_32BGRA; CFNumberRef nfmt = CFNumberCreate(NULL, kCFNumberIntType, &fmt); CFDictionarySetValue(attrs, kCVPixelBufferPixelFormatTypeKey, nfmt); @@ -204,7 +204,7 @@ static int dec_make_session(struct vid_dec *d) { VTDecompressionOutputCallbackRecord cb = { dec_cb, d }; OSStatus st = VTDecompressionSessionCreate(NULL, d->fd, NULL, attrs, - &cb, &d->sess); + &cb, &d->sess); CFRelease(attrs); if (st != noErr) { VLOG("VTDecompressionSessionCreate: %d", (int)st); return -2; } return 0; @@ -215,10 +215,9 @@ vid_dec *vid_dec_new(void) { } int vid_dec_frame(vid_dec *d, const uint8_t *data, int len, - uint8_t *out, int out_cap, int *w, int *h) { + uint8_t *out, int out_cap, int *w, int *h) { if (!d) return -1; - /* split annex-b; harvest sps/pps; collect vcl nalus */ const uint8_t *vcl[64]; size_t vcln[64]; int nvcl = 0; int i = 0, need_new_session = 0; while (i + 4 <= len) { @@ -236,7 +235,7 @@ int vid_dec_frame(vid_dec *d, const uint8_t *data, int int e = len; for (int j = s; j + 3 <= len; j++) { if (data[j] == 0 && data[j+1] == 0 && - (data[j+2] == 1 || (j + 4 <= len && data[j+2] == 0 && data[j+3] == 1))) { + (data[j+2] == 1 || (j + 4 <= len && data[j+2] == 0 && data[j+3] == 1))) { e = j; break; } } @@ -275,13 +274,13 @@ int vid_dec_frame(vid_dec *d, const uint8_t *data, int CMBlockBufferRef bb = NULL; if (CMBlockBufferCreateWithMemoryBlock(NULL, avcc, total, kCFAllocatorMalloc, - NULL, 0, total, 0, &bb) != noErr) { + NULL, 0, total, 0, &bb) != noErr) { free(avcc); return -3; } CMSampleBufferRef sb = NULL; size_t ssz = total; if (CMSampleBufferCreate(NULL, bb, true, NULL, NULL, d->fd, 1, 0, NULL, 1, - &ssz, &sb) != noErr) { + &ssz, &sb) != noErr) { CFRelease(bb); return -3; } blob - 58ad6a117543b270300d112ee55154ef79b4414a blob + f9f5bc20cfb5e113fa4cfd175fa34a777ea0f0fa --- src/vid_win.c +++ src/vid_win.c @@ -9,16 +9,16 @@ #include DEFINE_GUID(MILNA_AVLowLatencyMode, - 0x9c27891a, 0xed7a, 0x40e1, 0x88, 0xe8, 0xb2, 0x27, 0x27, 0xa0, 0x24, 0xee); + 0x9c27891a, 0xed7a, 0x40e1, 0x88, 0xe8, 0xb2, 0x27, 0x27, 0xa0, 0x24, 0xee); DEFINE_GUID(MILNA_AVEncVideoForceKeyFrame, - 0x2c0cf8be, 0x05ae, 0x4641, 0xa4, 0xf8, 0x3b, 0x53, 0xd8, 0xda, 0x59, 0xf5); + 0x2c0cf8be, 0x05ae, 0x4641, 0xa4, 0xf8, 0x3b, 0x53, 0xd8, 0xda, 0x59, 0xf5); static HRESULT attr_set_2x32(IMFAttributes *a, const GUID *k, - UINT32 hi, UINT32 lo) { + UINT32 hi, UINT32 lo) { return IMFAttributes_SetUINT64(a, k, ((UINT64)hi << 32) | lo); } static void attr_get_2x32(IMFAttributes *a, const GUID *k, - UINT32 *hi, UINT32 *lo) { + UINT32 *hi, UINT32 *lo) { UINT64 v = 0; IMFAttributes_GetUINT64(a, k, &v); *hi = (UINT32)(v >> 32); *lo = (UINT32)v; @@ -59,9 +59,9 @@ static void bgra_to_nv12(const uint8_t *src, int w, in } } -static void nv12_to_bgra(const uint8_t *Y, int ystride, - const uint8_t *UV, int uvstride, - int w, int h, uint8_t *dst) { +void vid_nv12_to_bgra(const uint8_t *Y, int ystride, + const uint8_t *UV, int uvstride, + int w, int h, uint8_t *dst) { for (int y = 0; y < h; y++) { const uint8_t *yr = Y + (size_t)y * ystride; const uint8_t *uv = UV + (size_t)(y / 2) * uvstride; @@ -81,7 +81,7 @@ static void nv12_to_bgra(const uint8_t *Y, int ystride } static IMFSample *mk_sample(const uint8_t *data, DWORD len, - LONGLONG t100ns, LONGLONG dur100ns) { + LONGLONG t100ns, LONGLONG dur100ns) { IMFMediaBuffer *buf = NULL; IMFSample *s = NULL; if (FAILED(MFCreateMemoryBuffer(len, &buf))) return NULL; BYTE *p; DWORD max; @@ -98,12 +98,28 @@ static IMFSample *mk_sample(const uint8_t *data, DWORD return s; } +static int ensure_out(IMFSample **s, IMFMediaBuffer **b, DWORD *cap, DWORD need) { + if (*cap >= need) return 0; + if (*b) IMFMediaBuffer_Release(*b); + if (*s) IMFSample_Release(*s); + *b = NULL; *s = NULL; + if (FAILED(MFCreateSample(s))) { *s = NULL; return -1; } + if (FAILED(MFCreateMemoryBuffer(need, b))) { + IMFSample_Release(*s); + *s = NULL; *b = NULL; + return -1; + } + IMFSample_AddBuffer(*s, *b); + *cap = need; + return 0; +} + struct vid_enc { IMFTransform *mft; ICodecAPI *capi; int hw; IMFMediaEventGenerator *evgen; - IMFSample *osample; /* reused output sample (no per-frame alloc) */ + IMFSample *osample; IMFMediaBuffer *obuffer; DWORD obuf_cap; int w, h, fps; @@ -122,33 +138,32 @@ vid_enc *vid_enc_new(int w, int h, int fps, int kbps) MFT_REGISTER_TYPE_INFO oti = { MFMediaType_Video, MFVideoFormat_H264 }; IMFActivate **acts = NULL; UINT32 nact = 0; UINT32 flags = MFT_ENUM_FLAG_HARDWARE | MFT_ENUM_FLAG_ASYNCMFT | - MFT_ENUM_FLAG_SORTANDFILTER; + MFT_ENUM_FLAG_SORTANDFILTER; if (SUCCEEDED(MFTEnumEx(MFT_CATEGORY_VIDEO_ENCODER, flags, - NULL, &oti, &acts, &nact)) && nact) { + NULL, &oti, &acts, &nact)) && nact) { for (UINT32 i = 0; i < nact && !e->mft; i++) { if (SUCCEEDED(IMFActivate_ActivateObject(acts[i], - &IID_IMFTransform, (void **)&e->mft))) { + &IID_IMFTransform, (void **)&e->mft))) { WCHAR nm[128] = {0}; UINT32 nl = 0; IMFActivate_GetString(acts[i], - &MFT_FRIENDLY_NAME_Attribute, nm, 128, &nl); + &MFT_FRIENDLY_NAME_Attribute, nm, 128, &nl); VLOG("hw h264 encoder: %ls", nm); e->hw = 1; } IMFActivate_Release(acts[i]); } - for (UINT32 i = 0; i < nact; i++) ; /* released above on use */ CoTaskMemFree(acts); } } - if (!e->mft) { /* software fallback (every windows, incl. VMs) */ + if (!e->mft) { if (FAILED(CoCreateInstance(&CLSID_CMSH264EncoderMFT, NULL, - CLSCTX_INPROC_SERVER, &IID_IMFTransform, (void **)&e->mft))) { + CLSCTX_INPROC_SERVER, &IID_IMFTransform, (void **)&e->mft))) { VLOG("h264 encoder mft unavailable"); free(e); return NULL; } VLOG("sw h264 encoder (no hardware mft found)"); } - /* an async hardware MFT must be unlocked before use */ + if (e->hw) { IMFAttributes *a = NULL; if (SUCCEEDED(IMFTransform_GetAttributes(e->mft, &a))) { @@ -170,13 +185,13 @@ vid_enc *vid_enc_new(int w, int h, int fps, int kbps) IMFMediaType_SetGUID(ot, &MF_MT_SUBTYPE, &MFVideoFormat_H264); IMFMediaType_SetUINT32(ot, &MF_MT_AVG_BITRATE, (UINT32)(kbps * 1000)); attr_set_2x32((IMFAttributes *)ot, &MF_MT_FRAME_SIZE, - (UINT32)w, (UINT32)h); + (UINT32)w, (UINT32)h); attr_set_2x32((IMFAttributes *)ot, &MF_MT_FRAME_RATE, (UINT32)fps, 1); attr_set_2x32((IMFAttributes *)ot, &MF_MT_PIXEL_ASPECT_RATIO, 1, 1); IMFMediaType_SetUINT32(ot, &MF_MT_INTERLACE_MODE, - MFVideoInterlace_Progressive); + MFVideoInterlace_Progressive); IMFMediaType_SetUINT32(ot, &MF_MT_MPEG2_PROFILE, - eAVEncH264VProfile_Main); + eAVEncH264VProfile_Main); HRESULT hr = IMFTransform_SetOutputType(e->mft, 0, ot, 0); IMFMediaType_Release(ot); if (FAILED(hr)) { VLOG("enc SetOutputType: 0x%lx", hr); goto fail; } @@ -186,10 +201,10 @@ vid_enc *vid_enc_new(int w, int h, int fps, int kbps) IMFMediaType_SetGUID(it, &MF_MT_MAJOR_TYPE, &MFMediaType_Video); IMFMediaType_SetGUID(it, &MF_MT_SUBTYPE, &MFVideoFormat_NV12); attr_set_2x32((IMFAttributes *)it, &MF_MT_FRAME_SIZE, - (UINT32)w, (UINT32)h); + (UINT32)w, (UINT32)h); attr_set_2x32((IMFAttributes *)it, &MF_MT_FRAME_RATE, (UINT32)fps, 1); IMFMediaType_SetUINT32(it, &MF_MT_INTERLACE_MODE, - MFVideoInterlace_Progressive); + MFVideoInterlace_Progressive); hr = IMFTransform_SetInputType(e->mft, 0, it, 0); IMFMediaType_Release(it); if (FAILED(hr)) { VLOG("enc SetInputType: 0x%lx", hr); goto fail; } @@ -199,7 +214,7 @@ vid_enc *vid_enc_new(int w, int h, int fps, int kbps) if (e->hw) IMFTransform_QueryInterface(e->mft, &IID_IMFMediaEventGenerator, - (void **)&e->evgen); + (void **)&e->evgen); IMFTransform_ProcessMessage(e->mft, MFT_MESSAGE_NOTIFY_BEGIN_STREAMING, 0); IMFTransform_ProcessMessage(e->mft, MFT_MESSAGE_NOTIFY_START_OF_STREAM, 0); e->w = w; e->h = h; e->fps = fps; @@ -219,18 +234,8 @@ static int enc_drain(struct vid_enc *e, uint8_t *out, IMFTransform_GetOutputStreamInfo(e->mft, 0, &osi); DWORD need = osi.cbSize ? osi.cbSize : 1024 * 1024; - if (e->obuf_cap < need) { - if (e->obuffer) IMFMediaBuffer_Release(e->obuffer); - if (e->osample) IMFSample_Release(e->osample); - e->obuffer = NULL; e->osample = NULL; - if (FAILED(MFCreateMemoryBuffer(need, &e->obuffer))) return total; - if (FAILED(MFCreateSample(&e->osample))) { - IMFMediaBuffer_Release(e->obuffer); e->obuffer = NULL; - return total; - } - IMFSample_AddBuffer(e->osample, e->obuffer); - e->obuf_cap = need; - } + if (ensure_out(&e->osample, &e->obuffer, &e->obuf_cap, need)) + return total; IMFMediaBuffer_SetCurrentLength(e->obuffer, 0); MFT_OUTPUT_DATA_BUFFER odb = {0}; odb.pSample = e->osample; @@ -251,13 +256,12 @@ static int enc_drain(struct vid_enc *e, uint8_t *out, if (SUCCEEDED(IMFMediaBuffer_Lock(got, &p, &max, &len))) { int has_sps = (len > 4 && (p[4] & 0x1f) == 7); if (clean && !has_sps && !e->seqhdr) { - /* fetch SPS/PPS once from the negotiated type */ IMFMediaType *cur = NULL; if (SUCCEEDED(IMFTransform_GetOutputCurrentType( - e->mft, 0, &cur))) { + e->mft, 0, &cur))) { UINT8 *blob = NULL; UINT32 bn = 0; if (SUCCEEDED(IMFMediaType_GetAllocatedBlob(cur, - &MF_MT_MPEG_SEQUENCE_HEADER, &blob, &bn)) && bn) { + &MF_MT_MPEG_SEQUENCE_HEADER, &blob, &bn)) && bn) { e->seqhdr = malloc(bn); if (e->seqhdr) { memcpy(e->seqhdr, blob, bn); @@ -269,7 +273,7 @@ static int enc_drain(struct vid_enc *e, uint8_t *out, } } if (clean && !has_sps && e->seqhdr && - total + (int)e->seqhdr_n <= cap) { + total + (int)e->seqhdr_n <= cap) { memcpy(out + total, e->seqhdr, e->seqhdr_n); total += (int)e->seqhdr_n; } @@ -286,7 +290,7 @@ static int enc_drain(struct vid_enc *e, uint8_t *out, } int vid_enc_frame(vid_enc *e, const uint8_t *bgra, int force_key, - uint8_t *out, int cap, int *is_key) { + uint8_t *out, int cap, int *is_key) { if (!e) return -1; if (is_key) *is_key = 0; if (force_key && e->capi) { @@ -297,11 +301,11 @@ int vid_enc_frame(vid_enc *e, const uint8_t *bgra, int bgra_to_nv12(bgra, e->w, e->h, e->nv12); LONGLONG dur = 10000000LL / e->fps; IMFSample *s = mk_sample(e->nv12, (DWORD)((size_t)e->w * e->h * 3 / 2), - e->pts * dur, dur); + e->pts * dur, dur); if (!s) return -2; e->pts++; - if (e->evgen) { /* async hardware MFT: event-driven */ + if (e->evgen) { int got_out = 0, fed = 0, total = 0, guard = 0; while (!got_out && guard++ < 16) { IMFMediaEvent *ev = NULL; @@ -340,11 +344,11 @@ void vid_enc_del(vid_enc *e) { struct vid_dec { IMFTransform *mft; - int out_w, out_h; /* display size (aperture-cropped) */ - int buf_w, buf_h; /* coded size */ + int out_w, out_h; + int buf_w, buf_h; int64_t pts; int started; - IMFSample *osample; /* reused output sample */ + IMFSample *osample; IMFMediaBuffer *obuffer; DWORD obuf_cap; }; @@ -352,10 +356,10 @@ struct vid_dec { static int dec_negotiate_output(struct vid_dec *d) { IMFMediaType *ot = NULL; for (DWORD i = 0; SUCCEEDED(IMFTransform_GetOutputAvailableType( - d->mft, 0, i, &ot)); i++) { + d->mft, 0, i, &ot)); i++) { GUID sub; if (SUCCEEDED(IMFMediaType_GetGUID(ot, &MF_MT_SUBTYPE, &sub)) && - IsEqualGUID(&sub, &MFVideoFormat_NV12)) { + IsEqualGUID(&sub, &MFVideoFormat_NV12)) { if (FAILED(IMFTransform_SetOutputType(d->mft, 0, ot, 0))) { IMFMediaType_Release(ot); return -1; } @@ -365,8 +369,8 @@ static int dec_negotiate_output(struct vid_dec *d) { d->out_w = (int)w; d->out_h = (int)h; MFVideoArea area; if (SUCCEEDED(IMFMediaType_GetBlob(ot, - &MF_MT_MINIMUM_DISPLAY_APERTURE, - (UINT8 *)&area, sizeof(area), NULL))) { + &MF_MT_MINIMUM_DISPLAY_APERTURE, + (UINT8 *)&area, sizeof(area), NULL))) { d->out_w = area.Area.cx; d->out_h = area.Area.cy; } @@ -384,7 +388,7 @@ vid_dec *vid_dec_new(void) { struct vid_dec *d = calloc(1, sizeof(*d)); if (!d) return NULL; if (FAILED(CoCreateInstance(&CLSID_CMSH264DecoderMFT, NULL, - CLSCTX_INPROC_SERVER, &IID_IMFTransform, (void **)&d->mft))) { + CLSCTX_INPROC_SERVER, &IID_IMFTransform, (void **)&d->mft))) { VLOG("h264 decoder mft unavailable"); free(d); return NULL; } @@ -403,18 +407,18 @@ vid_dec *vid_dec_new(void) { VLOG("dec SetInputType: 0x%lx", hr); IMFTransform_Release(d->mft); free(d); return NULL; } - dec_negotiate_output(d); /* may succeed only after first frames */ + dec_negotiate_output(d); return d; } int vid_dec_frame(vid_dec *d, const uint8_t *data, int len, - uint8_t *out, int out_cap, int *w, int *h) { + uint8_t *out, int out_cap, int *w, int *h) { if (!d) return -1; if (!d->started) { IMFTransform_ProcessMessage(d->mft, - MFT_MESSAGE_NOTIFY_BEGIN_STREAMING, 0); + MFT_MESSAGE_NOTIFY_BEGIN_STREAMING, 0); IMFTransform_ProcessMessage(d->mft, - MFT_MESSAGE_NOTIFY_START_OF_STREAM, 0); + MFT_MESSAGE_NOTIFY_START_OF_STREAM, 0); d->started = 1; } IMFSample *s = mk_sample(data, (DWORD)len, d->pts * 666666LL, 666666LL); @@ -427,18 +431,8 @@ int vid_dec_frame(vid_dec *d, const uint8_t *data, int for (;;) { if (d->buf_w == 0 && dec_negotiate_output(d) != 0) return 0; DWORD need = (DWORD)((size_t)d->buf_w * d->buf_h * 3 / 2); - if (d->obuf_cap < need) { - if (d->obuffer) IMFMediaBuffer_Release(d->obuffer); - if (d->osample) IMFSample_Release(d->osample); - d->obuffer = NULL; d->osample = NULL; - if (FAILED(MFCreateSample(&d->osample)) || - FAILED(MFCreateMemoryBuffer(need, &d->obuffer))) { - if (d->osample) { IMFSample_Release(d->osample); d->osample = NULL; } - return -2; - } - IMFSample_AddBuffer(d->osample, d->obuffer); - d->obuf_cap = need; - } + if (ensure_out(&d->osample, &d->obuffer, &d->obuf_cap, need)) + return -2; IMFMediaBuffer_SetCurrentLength(d->obuffer, 0); MFT_OUTPUT_DATA_BUFFER odb = {0}; odb.pSample = d->osample; @@ -453,7 +447,7 @@ int vid_dec_frame(vid_dec *d, const uint8_t *data, int return 0; if (FAILED(hr)) return -3; - /* got a picture */ + int ok = 0; if ((int64_t)d->out_w * d->out_h * 4 <= out_cap) { IMFMediaBuffer *got = NULL; @@ -463,8 +457,8 @@ int vid_dec_frame(vid_dec *d, const uint8_t *data, int if (SUCCEEDED(IMFMediaBuffer_Lock(got, &p, &bmax, &blen))) { const uint8_t *Y = p; const uint8_t *UV = p + (size_t)d->buf_w * d->buf_h; - nv12_to_bgra(Y, d->buf_w, UV, d->buf_w, - d->out_w, d->out_h, out); + vid_nv12_to_bgra(Y, d->buf_w, UV, d->buf_w, + d->out_w, d->out_h, out); IMFMediaBuffer_Unlock(got); *w = d->out_w; *h = d->out_h; ok = 1; blob - /dev/null blob + 8c3eda4b65170556ee7ac5904e909af809683a5b (mode 755) Binary files /dev/null and milna.new differ blob - a7836ecd0f19d6fe340ed999d6e5ba54801ac466 blob + c15995a4b6ce0dffe0f7d9ff0c7313084b0ceb71 --- srv/relay.go +++ srv/relay.go @@ -38,33 +38,45 @@ type member struct { pub string room string conn net.Conn - wmu sync.Mutex // serializes writes to conn + wmu sync.Mutex token [16]byte - udp *net.UDPAddr // bound media address (nil until hello) - want int // simulcast: which video layer this member receives + udp *net.UDPAddr + want int } func (m *member) send(v msg) { b, _ := json.Marshal(v) m.wmu.Lock() defer m.wmu.Unlock() + + m.conn.SetWriteDeadline(time.Now().Add(5 * time.Second)) m.conn.Write(append(b, '\n')) } type hub struct { mu sync.Mutex - members map[string]*member // id -> member - rooms map[string]map[string]*member // room -> id -> member + members map[string]*member + rooms map[string]map[string]*member byToken map[[16]byte]*member - byAddr map[string]*member // udp addr string -> member + byAddr map[string]*member udp *net.UDPConn nextID int } +type delivery struct { + to *member + v msg +} + +func deliver(d []delivery) { + for _, x := range d { + x.to.send(x.v) + } +} + func (h *hub) join(m *member, room, name, pub string) { h.mu.Lock() - defer h.mu.Unlock() - h.leaveLocked(m) + out := h.leaveLocked(m) m.room, m.name, m.pub = room, name, pub if h.rooms[room] == nil { @@ -73,11 +85,13 @@ func (h *hub) join(m *member, room, name, pub string) const maxRoom = 16 if len(h.rooms[room]) >= maxRoom { - m.send(msg{Type: "full"}) m.room = "" + out = append(out, delivery{m, msg{Type: "full"}}) + h.mu.Unlock() + deliver(out) return } - // pick the lowest free slot (u8, room-scoped) + used := map[int]bool{} for _, o := range h.rooms[room] { used[o.slot] = true @@ -89,22 +103,26 @@ func (h *hub) join(m *member, room, name, pub string) } } for _, o := range h.rooms[room] { - m.send(msg{Type: "peer", ID: o.id, Slot: o.slot, Name: o.name, Pub: o.pub}) - o.send(msg{Type: "peer-joined", ID: m.id, Slot: m.slot, Name: m.name, Pub: m.pub}) + out = append(out, delivery{m, msg{Type: "peer", ID: o.id, Slot: o.slot, Name: o.name, Pub: o.pub}}) + out = append(out, delivery{o, msg{Type: "peer-joined", ID: m.id, Slot: m.slot, Name: m.name, Pub: m.pub}}) } h.rooms[room][m.id] = m + n := len(h.rooms[room]) + h.mu.Unlock() + deliver(out) log.Printf("%s (%s) joined %s slot %d (%d in room)", - m.id, m.name, room, m.slot, len(h.rooms[room])) + m.id, m.name, room, m.slot, n) } -func (h *hub) leaveLocked(m *member) { +func (h *hub) leaveLocked(m *member) []delivery { if m.room == "" { - return + return nil } + var out []delivery if r := h.rooms[m.room]; r != nil { delete(r, m.id) for _, o := range r { - o.send(msg{Type: "peer-left", ID: m.id, Slot: m.slot}) + out = append(out, delivery{o, msg{Type: "peer-left", ID: m.id, Slot: m.slot}}) } if len(r) == 0 { delete(h.rooms, m.room) @@ -115,6 +133,7 @@ func (h *hub) leaveLocked(m *member) { m.udp = nil } m.room = "" + return out } func (h *hub) relayCtl(from *member, to string, v msg) { @@ -141,7 +160,7 @@ func (h *hub) udpLoop() { continue } switch buf[1] { - case 1: // hello: bind addr by token, echo back + case 1: if n < 8+16 { continue } @@ -156,8 +175,8 @@ func (h *hub) udpLoop() { h.byAddr[addr.String()] = m } h.mu.Unlock() - h.udp.WriteToUDP(buf[:n], addr) // confirm - case 2, 3, 4: + h.udp.WriteToUDP(buf[:n], addr) + case 2, 3, 4: layer := -1 if buf[1] == 3 { layer = int(buf[3]>>1) & 3 @@ -208,22 +227,25 @@ func (h *hub) serve(conn net.Conn) { m.send(msg{Type: "joined", Slot: m.slot}) case "leave": h.mu.Lock() - h.leaveLocked(m) + out := h.leaveLocked(m) h.mu.Unlock() + deliver(out) case "key", "kfreq": h.relayCtl(m, v.To, v) - case "state": // mute/deafen: broadcast to everyone else in the room + case "state": + var out []delivery h.mu.Lock() v.From = m.id if r := h.rooms[m.room]; r != nil { for _, o := range r { if o != m { - o.send(v) + out = append(out, delivery{o, v}) } } } h.mu.Unlock() - case "layer": // simulcast subscription: which layer to receive + deliver(out) + case "layer": h.mu.Lock() if v.Want >= 0 && v.Want <= 3 { m.want = v.Want @@ -233,10 +255,11 @@ func (h *hub) serve(conn net.Conn) { } h.mu.Lock() - h.leaveLocked(m) + out := h.leaveLocked(m) delete(h.members, m.id) delete(h.byToken, m.token) h.mu.Unlock() + deliver(out) conn.Close() log.Printf("%s disconnected", m.id) } @@ -257,6 +280,9 @@ func main() { if err != nil { log.Fatal(err) } + + h.udp.SetReadBuffer(1 << 20) + h.udp.SetWriteBuffer(1 << 20) go h.udpLoop() l, err := net.Listen("tcp", port) blob - 6664f7f169ef0d88edfd5b643045a05c909ee98e blob + b321518dbe333495f3c050144bb2253676dc80ab --- win-deps.sh +++ win-deps.sh @@ -2,14 +2,18 @@ set -e ARCH=${1:-x86_64} case "$ARCH" in - x86_64) HOST=x86_64-w64-mingw32 ;; - arm64) HOST=aarch64-w64-mingw32 ;; + x86_64) HOST=x86_64-w64-mingw32; PROC=x86_64 ;; + arm64) HOST=aarch64-w64-mingw32; PROC=aarch64 ;; *) echo "arch: x86_64 | arm64"; exit 1 ;; esac command -v $HOST-clang >/dev/null 2>&1 || command -v $HOST-gcc >/dev/null 2>&1 || { echo "no $HOST toolchain. on macos: brew install llvm-mingw" exit 1 } +command -v cmake >/dev/null 2>&1 || { + echo "cmake needed for fltk. on macos: brew install cmake" + exit 1 +} PFX="$(pwd)/deps-win-$ARCH" BLD="$(pwd)/.win-deps-build" mkdir -p "$BLD" && cd "$BLD" @@ -35,5 +39,27 @@ build opus-1.5.2 --disable-doc --disable-extra-program build speexdsp-SpeexDSP-1.2.1 --disable-examples build libsodium-1.0.20-RELEASE +if [ ! -f "$PFX/lib/libfltk.a" ]; then + fetch https://github.com/fltk/fltk/releases/download/release-1.4.5/fltk-1.4.5-source.tar.gz fltk-1.4.5 + CCBIN=$(command -v $HOST-clang || command -v $HOST-gcc) + CXXBIN=$(command -v $HOST-clang++ || command -v $HOST-g++) + RCFLAG="" + command -v $HOST-windres >/dev/null 2>&1 && RCFLAG="-DCMAKE_RC_COMPILER=$HOST-windres" + cmake -S fltk-1.4.5 -B fltk-build \ + -DCMAKE_SYSTEM_NAME=Windows -DCMAKE_SYSTEM_PROCESSOR=$PROC \ + -DCMAKE_C_COMPILER="$CCBIN" -DCMAKE_CXX_COMPILER="$CXXBIN" $RCFLAG \ + -DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER \ + -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY \ + -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY \ + -DCMAKE_BUILD_TYPE=MinSizeRel -DCMAKE_INSTALL_PREFIX="$PFX" \ + -DFLTK_BUILD_TEST=OFF -DFLTK_BUILD_EXAMPLES=OFF -DFLTK_BUILD_FLUID=OFF \ + -DFLTK_BUILD_GL=OFF -DFLTK_BUILD_FORMS=OFF \ + -DFLTK_USE_SYSTEM_ZLIB=OFF -DFLTK_USE_SYSTEM_LIBPNG=OFF \ + -DFLTK_USE_SYSTEM_LIBJPEG=OFF >/dev/null + cmake --build fltk-build -j8 >/dev/null + cmake --install fltk-build >/dev/null + echo "fltk-1.4.5 -> ok" +fi + echo "deps in $PFX" echo "build: WINARCH=$ARCH ./build.sh win"