diff --git a/.github/ISSUE_TEMPLATE/issue.md b/.github/ISSUE_TEMPLATE/issue.md index 8e499465ce..8ee7d34d1f 100644 --- a/.github/ISSUE_TEMPLATE/issue.md +++ b/.github/ISSUE_TEMPLATE/issue.md @@ -12,7 +12,7 @@ Welcome to v86's issue tracker! We use this tracker for bug reports or feature requests. For user support, questions or general comments, use the chat at https://gitter.im/copy/v86 or the forum at https://github.com/copy/v86/discussions -Please don't ask for support for any version of Windows. There are many existing issues at https://github.com/copy/v86/issues?q=is%253Aissue+windows. See also docs/windows-xp.md. +Please don't ask for support for any version of Windows. There are many existing issues at https://github.com/copy/v86/issues?q=is%253Aissue+windows. See also docs/windows-nt.md and docs/windows-9x.md. Before reporting OS incompatibilities, check existing issues and the compatibility section of the readme. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20d02d3656..dd4ac97158 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,7 +53,7 @@ jobs: run: make rustfmt - name: Fetch kvm-unit-test cache - uses: actions/cache@v3 + uses: actions/cache@v4 id: cache-kvm-unit-test with: path: tests/kvm-unit-tests/ @@ -67,7 +67,7 @@ jobs: run: tests/kvm-unit-tests/run.js tests/kvm-unit-tests/x86/realmode.flat - name: Fetch namsmtests cache - uses: actions/cache@v3 + uses: actions/cache@v4 id: cache-nasmtests with: path: tests/nasm/build/ @@ -83,7 +83,7 @@ jobs: run: make rust-test - name: Fetch image cache - uses: actions/cache@v3 + uses: actions/cache@v4 id: cache-images with: path: images/ @@ -115,7 +115,7 @@ jobs: run: make expect-tests - name: Upload the artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: v86 path: | @@ -138,7 +138,7 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} - name: Get artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: v86 path: build diff --git a/Makefile b/Makefile index 6a9c31df75..c932dcb52c 100644 --- a/Makefile +++ b/Makefile @@ -79,7 +79,7 @@ CARGO_FLAGS_SAFE=\ CARGO_FLAGS=$(CARGO_FLAGS_SAFE) -C target-feature=+bulk-memory -C target-feature=+multivalue -C target-feature=+simd128 CORE_FILES=const.js config.js io.js main.js lib.js buffer.js ide.js pci.js floppy.js \ - memory.js dma.js pit.js vga.js vga_text.js ps2.js rtc.js uart.js \ + memory.js dma.js pit.js vga.js ps2.js rtc.js uart.js \ acpi.js apic.js ioapic.js \ state.js ne2k.js sb16.js virtio.js virtio_console.js virtio_net.js \ bus.js log.js cpu.js debug.js \ @@ -318,10 +318,12 @@ rust-test-intensive: QUICKCHECK_TESTS=100000000 make rust-test api-tests: all-debug -# ./tests/api/clean-shutdown.js \ - ./tests/api/reset.js \ - ./tests/api/floppy-insert-eject.js \ - ./tests/api/serial.js \ + ./tests/api/clean-shutdown.js + ./tests/api/state.js + ./tests/api/reset.js + #./tests/api/floppy-insert-eject.js # disabled for now, sometimes hangs + ./tests/api/serial.js + ./tests/api/reboot.js all-tests: eslint kvm-unit-test qemutests qemutests-release jitpagingtests api-tests nasmtests nasmtests-force-jit tests expect-tests # Skipping: diff --git a/Readme.md b/Readme.md index 02eeaed549..fa47e7808a 100644 --- a/Readme.md +++ b/Readme.md @@ -62,7 +62,8 @@ list of emulated hardware: [Networking](docs/networking.md) — [Alpine Linux guest setup](tools/docker/alpine/) — [Arch Linux guest setup](docs/archlinux.md) — -[Windows 2000/XP guest setup](docs/windows-xp.md) — +[Windows NT guest setup](docs/windows-nt.md) — +[Windows 9x guest setup](docs/windows-9x.md) — [9p filesystem](docs/filesystem.md) — [Linux rootfs on 9p](docs/linux-9p-image.md) — [Profiling](docs/profiling.md) — @@ -94,8 +95,9 @@ Here's an overview of the operating systems supported in v86: - Windows 1, 3.x, 95, 98, ME, NT and 2000 work reasonably well. - In Windows 2000 and higher the PC type has to be changed from ACPI PC to Standard PC - There are some known boot issues ([#250](https://github.com/copy/v86/issues/250), [#433](https://github.com/copy/v86/issues/433), [#507](https://github.com/copy/v86/issues/507), [#555](https://github.com/copy/v86/issues/555), [#620](https://github.com/copy/v86/issues/620), [#645](https://github.com/copy/v86/issues/645)) + - See [Windows 9x guest setup](docs/windows-9x.md) - Windows XP, Vista and 8 work under certain conditions (see [#86](https://github.com/copy/v86/issues/86), [#208](https://github.com/copy/v86/issues/208)) - - See [Windows 2000/XP guest setup](docs/windows-xp.md) + - See [Windows NT guest setup](docs/windows-nt.md) - Many hobby operating systems work. - 9front works. - Plan 9 doesn't work. @@ -166,6 +168,7 @@ See [tests/Readme.md](tests/Readme.md) for more information. - [Programatically using the serial terminal](examples/serial.html) - [A Lua interpreter](examples/lua.html) - [Two instances in one window](examples/two_instances.html) +- [Networking between browser windows/tabs using the Broadcast Channel API](examples/broadcast-network.html) - [Saving and restoring emulator state](examples/save_restore.html) Using v86 for your own purposes is as easy as: diff --git a/debug.html b/debug.html index 57594cb869..829aa17ca6 100644 --- a/debug.html +++ b/debug.html @@ -1,7 +1,7 @@ -Virtual x86 (debug) +v86 (debug) @@ -20,7 +20,6 @@ - diff --git a/docs/windows-9x.md b/docs/windows-9x.md new file mode 100644 index 0000000000..118e7961c2 --- /dev/null +++ b/docs/windows-9x.md @@ -0,0 +1,106 @@ +## Installing using QEMU + +Recommended versions: + - Windows 95 OSR2(.5) + - Windows 98 Second Edition (SE) + +------------- + +1. Create a disk image (up to 2 GB): +```sh +qemu-img create -f raw hdd.img M +``` +2. Run QEMU with the following settings: +```sh +qemu-system-i386 -m 128 -M pc,acpi=off -hda hdd.img +``` + - add `-cdrom /path/to/installCD.iso`, if you use a CD version. + - add `-fda /path/to/boot_floppy.img -boot a`, if you use a floppy version or your install CD is non-bootable. + - (optionally) add `-device sb16` to enable sound + - (optionally) add `-nic user,model=ne2k_pci` or `-device ne2k_pci,netdev=<...>` to enable networking + +3. For Windows 98: select "Start Windows 98 Setup from CD-ROM". For Windows 95: select "Load NEC IDE CDROM driver" and run `fdisk` to create partition, restart emulator, run `format c:` and `D:\WIN95\SETUP`. + +4. To change floppy disk, press *Ctrl+Alt+2* to switch to the QEMU Monitor, run `change floppy0 /path/to/new_floppy_image` and press *Ctrl+Alt+1* to switch to VGA. +5. Follow the installation guide on the screen. +6. (optionally) If "Windows protection" errors appears on startup, apply [FIX95CPU](http://lonecrusader.x10host.com/fix95cpu.html) or [patcher9x](https://github.com/JHRobotics/patcher9x#installation). + +> [!TIP] +> For transfer files from host to guest, use [genisoimage](https://wiki.debian.org/genisoimage) ([UltraISO](https://www.ultraiso.com/) and [PowerISO](https://www.poweriso.com/) for Windows and Mac) for creating CD-ISO image or [dosfstools](https://github.com/dosfstools/dosfstools) ([WinImage](https://www.winimage.com/download.htm) for Windows) for creating floppy disk images, then mount the created image to QEMU. + +## Floppy disk support + +Currently, the floppy drive in v86 works only with MS-DOS compatibility mode. + +To check this: open the Start menu, click on "Control Panel" and "System", select "Performance" tab. +If it says *"Drive A is using MS-DOS compatibility mode file system"*, the floppy drive should work properly in v86. If not, try this solution: + +1. Click on "Device Manager" in "System Properties". +2. Open "Floppy disk controllers", select "Standard Floppy Disk Controller" and press "Remove" at the bottom. +3. Restart Windows. + +## Enabling True Color (32 bpp) + +The default VGA display driver only supports 640x480x8 video mode, to fix this, install **Universal VBE9x Video Display Driver**. + +> [!WARNING] +> After installing, DOS Mode (and other programs and games that require it) may not work properly. +> This is a problem in VBE9x, not v86, see [#110](https://github.com/copy/v86/issues/110). +> Also, this driver doesn't support DirectX, DirectDraw and OpenGL. + +1. Download driver from https://bearwindows.zcm.com.au/vbe9x.htm and unpack into Windows. +2. Right-click on the Desktop, click on "Properties". +3. Click "Advanced" > "Adapter" > "Change". +4. Press "Next", select "Display a of all the drivers in a specific location..." and press again "Next". +5. Press "Have Disk...", click "Browse" and go to folder with unpacked driver. Inside the folder with driver, should be folders like `032mb`, `064mb`, `128mb`. Choose a version based on needed video memory size (for example, `032mb`), then select `vbemp.inf` inside. +6. Select "VBE Miniport" adapter, press "OK" and "Next". +7. After installing, restart Windows. + +## CPU idling on Windows 95 +See about [installing AmnHLT](cpu-idling.md#windows-9x-using-amnhlt). + +## Enabling networking on Windows 95 (requires install CD) + +1. Open the Start menu, click on "Control Panel" and "Add New Hardware". +2. Press "Next", select "No" and select next options: + +``` +Hardware type: Network adapters +Manufacturers: Novell +Models: NE2000 Compatible +``` + +3. Press "Next" and restart Windows. +4. After restarting, right-click on "My computer", select "Propeties". +5. Open "Device Manager" tab, select "NE2000 Compatible" (in "Network adapters") and press "Properties" +6. Open "Resources", change values by selecting the properties and click on "Change Setting": + +``` +Interrupt Request: 10 +Input/Output Range: 0300 - 031F +``` + +7. In "Control Panel", open "Network", click on "Add", choose "Protocol" and select the following options: + +``` +Manufacturers: Microsoft +Network Protocols: TCP/IP +``` + +8. (optionally) Set "Primary Network Logon" to `Windows Logon`. + +## Enabling sound manually + +> [!NOTE] +> If you don't have an install CD, use the Sound Blaster 16 driver from https://www.claunia.com/qemu/drivers/index.html. + +1. Open "Start" menu, click on "Control Panel" and "Add New Hardware". +2. Press "Next", select "No" and select the following options: + +``` +Hardware type: Sound, video and game cotrollers +Manufacturers: Creative Labs +Models: Creative Labs Sound Blaster 16 or AWE-32 +``` + +3. Restart Windows. diff --git a/docs/windows-nt.md b/docs/windows-nt.md new file mode 100644 index 0000000000..e347488044 --- /dev/null +++ b/docs/windows-nt.md @@ -0,0 +1,169 @@ + + - [Windows NT 3.1](#windows-nt-31) / [3.51](#windows-nt-351) / [4.0](#windows-nt-40) + - [Windows 2000/XP](#windows-2000xp) + - [Windows Vista and newer](#windows-vista-and-newer) + +------------------------ +## Windows NT 3.1 + +### Installing using QEMU + +1. Install MS-DOS and [Oak CD-ROM Driver](https://www.dosdays.co.uk/topics/Software/optical_downloads.php). +2. Create 4 blank floppy disk images: + + - run `qemu-img create -f raw floppy.img 1440K` + - mount (`-fda floppy.img`) and run `format A:` in a VM + +3. Run QEMU with the following settings for installation: + +```sh +qemu-system-i386 -m 64 -hda hdd.img -cpu pentium -M pc,acpi=off -cdrom InstallCD.iso +``` + +4. Run `xcopy /v :\I386\ C:\install\` in a VM to copy all files, disable the CD-ROM driver. +5. Run QEMU with the following settings: + +```sh +qemu-system-i386 -m 64 -hda hdd.img -cpu pentium -M pc,acpi=off +``` + +6. Run `C:\install\winnt /F /C` in a VM. +7. Follow the setup instructions. To change floppy disk, press *Ctrl+Alt+2* to switch to the QEMU Monitor, run `change floppy0 /path/to/new_floppy_image` and press *Ctrl+Alt+1* to switch to VGA. + + +## Windows NT 3.51 + +### Installing + +> [!NOTE] +> In newer versions of QEMU, the Windows Setup may not work, you can use an older version of QEMU, PCem, 86Box or PCBox instead. + +1. If you install via MS-DOS, install [the Oak CD-ROM Driver](https://www.dosdays.co.uk/topics/Software/optical_downloads.php) and run `:\I386\WINNT /B`. +2. Follow the setup instructions. +3. After installing, download NT 3.51 SuperPack ([here](https://bearwindows.zcm.com.au/winnt351.htm#4) or [here](https://alter.org.ua/en/soft/nt_spack/nt3/)), unpack the archive into a Windows and copy files from `FAT32` (`SYS\FAT32`) and `RENEW` (`SYS\RENEW`) folders in `C:\WINNT35\system32\drivers` with replacing. + +### Enabling networking + +1. Open "Control Panel" > "Network", install Windows NT Networking (installation CD required). +2. In "Network Adapter Card Detection", press Continue three times, set `Network Adapter Card: Novell NE2000 Compatible Adapter`. +3. Set the following settings and click Continue: + +``` +IRQ Level: 10 +I/O Port Address: 0x300 +``` + +4. In "Bus Location", press OK. Check the boxes "TCP/IP Transport" and "Enable Automatic DHCP Configuration" in the next window. +5. In "TCP/IP Configuration", check the box "Enable Automatic DHCP Configuration". +6. Restart the VM. + + +## Windows NT 4.0 + +Recommended version: Windows NT 4.0 SP1 + +### Installing using QEMU + +1. Run QEMU with the following settings for installation: + +```sh +qemu-system-i386 -m 64 -hda hdd.img -cdrom InstallCD.iso -cpu pentium -M pc,acpi=off +``` + +2. On setup startup, press F5 and select "Standard PC". +3. Follow the setup instructions. + +### Running in v86 + +Due to a problem with CPUID, you need to add `cpuid_level: 2` and `acpi: false` to the V86 constructor (not supported in the UI): + +```js +var emulator = new V86({ + ... + cpuid_level: 2, + acpi: false +}); +``` + + +## Windows 2000/XP + +### Installing using QEMU + +1. Run QEMU with the following settings for installation: + +```sh +qemu-system-i386 -m 512 -hda hdd.img -cdrom InstallCD.iso +``` + +Optional: + - add `-device sb16` to enable sound + - add `-nic user,model=ne2k_pci` or `-device ne2k_pci,netdev=<...>` to enable networking + +2. Follow the setup instructions. +3. This step fixes the error `Uncaught RangeError: Maximum call stack size exceeded` in Chromium during Windows 2000/XP startup in v86. + +After installation, change the computer type to "Standard PC" as described [here](http://web.archive.org/web/20220528021535/https://www.scm-pc-card.de/file/manual/FAQ/acpi_uninstallation_windows_xp_english.pdf): +1. Open Start menu, right-click on "My Computer", select "Manage" +2. Open Device Manager, open Computer, right-click on "ACPI Uniprocessor PC" +3. Select "Update Driver..." > "No, not this time" +4. Select "Install from a list or specific location (Advanced)" > Next > "Don't search. I will choose the driver to install." +5. Choose "Standard PC", press Next > Finish. +6. Restart the VM, follow multiple "Found New Hardware Wizard" dialogs with default options. + +### Enabling True Color (for Windows 2000) + +> [!NOTE] +> This driver doesn't support DirectX, DirectDraw and OpenGL. + +1. Download driver from https://bearwindows.zcm.com.au/vbemp.htm and unpack into Windows. +2. Open Start menu, right-click on "My Computer", select "Manage" +3. Open Device Manager, open Computer and right-click on "Video Controller". +4. Press "Properties", select "Driver" tab and press "Update Driver". +5. Select "Display a list of the known drivers for this device...", choose "Display adapters". +5. Press "Have Disk...", click "Browse" and go to folder with unpacked driver. Go to `VBE20\W2K\PNP`, then select `vbemppnp.inf` inside. +6. Select "VBE Miniport" adapter, press "Yes" and "Next". +7. After installing, restart the VM. + +### Enabling sound + +*Source: [#1049](https://github.com/copy/v86/issues/1049)* + +1. Right-click on "My computer" > "System Properties", select "Hardware" tab, press "Hardware Wizard" +2. Press "Next" > "Add/Troubleshoot a device" > "Add a new device" +3. Select "No, I want to select the hardware from a list" > "Sound, video and game controllers" +4. Select the following options and press "Next": + +``` +Hardware type: Sound, video and game cotrollers +Manufacturers: Creative Technology Ltd. +Models: Sound Blaster 16 or AWE32 or compatible (WDM) +``` + + +## Windows Vista and newer + +### Installing using QEMU + +1. Run QEMU with the following settings for installation: + +```sh +qemu-system-i386 -m 1024 -hda hdd.img -cdrom InstallCD.iso +``` + +Optionally add `-accel kvm` (for Linux host), `-accel whpx` (for Windows host) or `-accel hvf` (for MacOS host) to use hypervisor acceleration. + +2. Follow the setup instructions. + +### Running in v86 + +Enable ACPI and set the memory size to 512 MB or more. + +### Enabling networking (ne2k) + +*Source: https://phaq.phunsites.net/2007/05/21/vista-on-xen-using-ne2000-in-favor-to-rtl8139/* + +1. Download https://phaq.phunsites.net/files/2007/05/drivercd.iso_.zip, unpack the archive, mount the ISO to the VM (`-cdrom path/to/drivercd.iso` or `change ide1-cd0 path/to/drivercd.iso` in QEMU Monitor), unpack the archive from CDROM into Windows. +2. Open Start Menu > "Control Panel" > "System" > "Device Manager" +3. Right-click on "Ethernet Controller" > "Update Driver Software", press "Browse my computer for driver software". +4. Click "Browse" and go to folder with unpacked driver, select `WIN2000` folder, press "Install this driver software anyway". diff --git a/docs/windows-xp.md b/docs/windows-xp.md deleted file mode 100644 index 2445cb43c7..0000000000 --- a/docs/windows-xp.md +++ /dev/null @@ -1,73 +0,0 @@ -*Most of this document also applies to Windows 2000.* - -You can download Windows 2000 from [WinWorld](https://winworldpc.com/download/413638c2-8d18-c39a-11c3-a4e284a2c3a5). -Use QEMU to create `winxp.img`: - -``` -qemu-img create winxp.img 2G -qemu-system-x86_64 -m 512 -drive file=winxp.img,format=raw -cdrom en_windows_xp_professional_with_service_pack_3_x86_cd_vl_x14-73974.iso -``` - -Follow the setup instructions. -This step fixes the error `Uncaught RangeError: Maximum call stack size exceeded` in Chromium during Windows 2000/XP startup in v86. - -After installation, change the computer type to "Standard PC" as described [here](http://web.archive.org/web/20220528021535/https://www.scm-pc-card.de/file/manual/FAQ/acpi_uninstallation_windows_xp_english.pdf): -Start > Right Click "My Computer" > Manage > -Device Manager > Computer > Right Click "ACPI Uniprocessor PC" > Update Driver... > -No, not this time > Next > Install from a list or specific location (Advanced) > Next > -Don't search. I will choose the driver to install. > Next > Standard PC > Next > Finish. -Restart the VM, follow multiple "Found New Hardware Wizard" dialogs with default options. - -Now, `winxp.img` is ready for v86. You can use [the website](https://copy.sh/v86/) to run it: -Specify `winxp.img` as a hard disk, and optionally set the memory size to 512 MB. -Or run it in a custom HTML file as described below. - -Get seabios.bin and vgabios.bin from [here](https://github.com/copy/v86/tree/master/bios), -and get libv86.js and v86.wasm from [releases](https://github.com/copy/v86/releases/tag/latest). -Create `winxp.htm` with this content (assuming all the files are in the same folder): - -```html - - - - - -
-
- -
-``` - -To open this HTML file locally, a HTTP server is needed. The standard Python server `python -m http.server` doesn't support HTTP range requests. -You can use [http-server](https://www.npmjs.com/package/http-server) or [devd](https://github.com/cortesi/devd). -Start the server (from the same folder as `winxp.htm`): -``` -npx http-server -``` -Open http://localhost:8080/winxp.htm in the browser. - -Windows XP load time (until start button becomes responsive) in Chromium on my computer: -* 3 min second time -* 4 min (first time or if cache is disabled) -* 12 min second time if Network tab in Developer Tools is open -* 17 min (first time or if cache is disabled) and Network tab in Developer Tools is open - -Sometimes Windows XP hangs after boot (before it is interactive) in v86, -displaying only desktop wallpaper without taskbar or desktop icons. diff --git a/examples/broadcast-network.html b/examples/broadcast-network.html new file mode 100644 index 0000000000..212891268b --- /dev/null +++ b/examples/broadcast-network.html @@ -0,0 +1,61 @@ + +Networking via Broadcast Channel API + + + + +
+
+ +
+ +
+# This example allows network across multiple browser tabs by using BroadcastChannels.
+
+# Configure a static IP
+ifconfig eth0 up arp 10.5.0.x
+
+# Ping by IP
+ping 10.5.0.x
+
+# Run a DNS server and send a query (10.5.0.x for server, 10.5.0.y for record)
+echo "anotherhost 10.5.0.y" | dnsd -c - -v    - server
+nslookup -type=a anotherhost 10.5.0.x         - client
+
+# Telnet calculator
+socat TCP-L:23,fork exec:bc
+
+# Simple HTTP server
+socat TCP-L:80,crlf,fork system:'echo HTTP/1.1 200 OK;echo;lua /root/test.lua'
+
diff --git a/examples/nodejs.js b/examples/nodejs.js index f08b54d473..c2ec8246d0 100755 --- a/examples/nodejs.js +++ b/examples/nodejs.js @@ -38,7 +38,7 @@ process.stdin.on("data", function(c) if(c === "\u0003") { // ctrl c - emulator.stop(); + emulator.destroy(); process.stdin.pause(); } else diff --git a/examples/nodejs_state.js b/examples/nodejs_state.js index e829cb093b..229f2c1456 100755 --- a/examples/nodejs_state.js +++ b/examples/nodejs_state.js @@ -42,7 +42,7 @@ process.stdin.on("data", async function(c) if(c === "\u0003") { // ctrl c - emulator.stop(); + emulator.destroy(); process.stdin.pause(); } else if(c === "\x1b\x4f\x51") diff --git a/index.html b/index.html index ee08215592..e3e540d8ae 100644 --- a/index.html +++ b/index.html @@ -1,7 +1,7 @@ -Virtual x86 +v86 @@ -10,64 +10,113 @@
-

Select profile

+
+ Family: + + + + + + + + UI: + + + Medium: + + + + Size: + + + + Status: + + + License: + + + Arch: + + + + + + Lang: + + + + + + +
+
+
+ - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Arch Linux 12 MB >_ - Complete Arch Linux with various compilers, networking and Xorg. Restored from snapshot.
Damn Small Linux 50 MB 💻 - Graphical Linux with 2.4 kernel, Firefox 2.0 and more. Takes 1 minute to boot.
Buildroot Linux 5.0 MB >_ - Minimal Linux with busybox, Lua, tests, internet access, ping, telnet and curl. Exchange files through /mnt/.
ReactOS 18 MB 💻 - Windows-compatible OS with QtWeb and Breakout. Restored from snapshot.
Windows 2000 22 MB 💻 - Including Pinball and Internet Explorer with internet access. Additional sectors are loaded as needed.
Windows 98 9.7 MB 💻 - Including Minesweeper and Internet Explorer with internet access. Additional sectors are loaded as needed.
Windows 95 4.6 MB 💻 - Restored from snapshot
Windows 3.1 15 MB 💻 - Takes 15 seconds to boot
Windows 1.01 0.6 MB 💻 - The first version of Microsoft Windows
MS-DOS 6.22 4.4 MB >_ - With Enhanced Tools, QBasic, vim, games and demos.
FreeDOS 0.5 MB >_ - With nasm, vim, debug.com, Rogue, some games and demos.
FreeBSD 17 MB >_ - FreeBSD 12.0 base install. Restored from snapshot.
OpenBSD 12 MB >_ - OpenBSD 6.6 base install. Restored from snapshot.
9front 4.4 MB 💻 - A Plan 9 fork.
Haiku 38 MB 💻 - An open-source operating system inspired by BeOS. Restored from snapshot. Includes network support.
SerenityOS 17 MB 💻 - A graphical Unix-like operating system. Restored from snapshot.
HelenOS 7.9 MB 💻 - A graphical operating system based on a multiserver microkernel design
FiwixOS 15 MB >_ - A Unix-like OS written from scratch. Includes Doom.
Android-x86 42 MB 💻 - An x86 port of the Android 1.6. Quite slow. Takes about 3 minutes to boot.
Oberon 1.2 MB 💻 - Native Oberon 2.3.6
KolibriOS 1.4 MB 💻 - Fast graphical OS written in Assembly
QNX 1.3 MB 💻 - QNX 4.05 Demo disk (no networking)
Snowdrop 0.3 MB >_ - A homebrew operating system from scratch, written in assembly language
Solar OS 0.3 MB 💻 - Simple graphical OS
Bootchess 512 B >_ - A tiny chess program written in the boot sector
SectorLISP 512 B >_ - A LISP interpreter that fits into the boot sector
NameSizeUIFamilyArchStatusSourceLangMediumNotes
Arch Linux 15+ MB Linux 32-bit Modern Open-source C 9pfs Xorg, Firefox, various compilers and more
Damn Small Linux 50 MB Linux 32-bit Historic Open-source C CD 4.11.rc2 with Firefox 2.0
Buildroot Linux 4.9 MB Linux 32-bit Modern Open-source C bzImage Lua, ping, curl, telnet
FreeBSD 16+ MB BSD 32-bit Modern Open-source C HD FreeBSD 12.0
OpenBSD 11+ MB BSD 32-bit Modern Open-source C HD OpenBSD 6.6
FiwixOS 15+ MB Unix-like 32-bit Modern Open-source C HD With Doom
SerenityOS 16+ MB Unix-like 32-bit Modern Open-source C++ HD Web browser, various games and demos
Haiku 41+ MB BeOS 32-bit Modern Open-source C++ HD Networking (WebPositive), OCaml, 2048, NetHack
Tiny Aros 17+ MB AmigaOS 32-bit Modern Open-source C CD AmigaOS-like graphical OS
ReactOS 17+ MB Windows-like 32-bit Modern Open-source C++ HD QtWeb, LBreakout2, OpenTTD, Bochs, TCC
Windows 1.01 0.7 MB Windows 16-bit Historic Proprietary ASM, C Floppy Reversi, Paint
Windows 95 19+ MB Windows 32-bit Historic Proprietary ASM, C HD Age of Empires, FASM, POV-Ray, Hover!
Windows 2000 23+ MB Windows 32-bit Historic Proprietary C++ HD IE 5, Pinball
MS-DOS 6.22 2.4+ MB DOS 16-bit Historic Proprietary ASM HD Doom, Sim City, OCaml 1.0, Turbo C and more
FreeDOS 0.6 MB DOS 16-bit Modern Open-source ASM, C Floppy nasm, vim, debug.com, Rogue, various demos
KolibriOS 1.3 MB Custom 32-bit Modern Open-source ASM Floppy Various apps, games and demos
QNX 4.05 1.4 MB Custom 32-bit Historic Proprietary C Floppy 1999 demo disk

-

Setup

+

Setup

@@ -126,7 +175,8 @@

Setup

- + diff --git a/src/browser/dummy_screen.js b/src/browser/dummy_screen.js index 2f7757d0b0..405dd818f4 100644 --- a/src/browser/dummy_screen.js +++ b/src/browser/dummy_screen.js @@ -48,6 +48,14 @@ function DummyScreenAdapter() is_graphical = graphical; }; + this.set_font_bitmap = function(height, width_9px, width_dbl, copy_8th_col, bitmap, bitmap_changed) + { + }; + + this.set_font_page = function(page_a, page_b) + { + }; + this.clear_screen = function() { }; diff --git a/src/browser/fake_network.js b/src/browser/fake_network.js index 39ea9ba393..3e1cf7b725 100644 --- a/src/browser/fake_network.js +++ b/src/browser/fake_network.js @@ -5,7 +5,6 @@ const ETHERTYPE_IPV4 = 0x0800; const ETHERTYPE_ARP = 0x0806; const ETHERTYPE_IPV6 = 0x86DD; - const IPV4_PROTO_ICMP = 1; const IPV4_PROTO_TCP = 6; const IPV4_PROTO_UDP = 17; @@ -18,60 +17,248 @@ const TWO_TO_32 = Math.pow(2, 32); const DHCP_MAGIC_COOKIE = 0x63825363; const V86_ASCII = [118, 56, 54]; +/* For the complete TCP state diagram see: + * + * https://en.wikipedia.org/wiki/File:Tcp_state_diagram_fixed_new.svg + * + * State TIME_WAIT is not needed, we can skip it and transition directly to CLOSED instead. + */ const TCP_STATE_CLOSED = "closed"; -const TCP_STATE_LISTEN = "listen"; +const TCP_STATE_SYN_RECEIVED = "syn-received"; +const TCP_STATE_SYN_SENT = "syn-sent"; +//const TCP_STATE_LISTEN = "listen"; const TCP_STATE_ESTABLISHED = "established"; const TCP_STATE_FIN_WAIT_1 = "fin-wait-1"; const TCP_STATE_CLOSE_WAIT = "close-wait"; const TCP_STATE_FIN_WAIT_2 = "fin-wait-2"; const TCP_STATE_LAST_ACK = "last-ack"; const TCP_STATE_CLOSING = "closing"; -const TCP_STATE_TIME_WAIT = "time-wait"; -const TCP_STATE_SYN_RECEIVED = "syn-received"; -const TCP_STATE_SYN_SENT = "syn-sent"; +//const TCP_STATE_TIME_WAIT = "time-wait"; + +// source: RFC6335, 6. Port Number Ranges +const TCP_DYNAMIC_PORT_START = 49152; +const TCP_DYNAMIC_PORT_END = 65535; +const TCP_DYNAMIC_PORT_RANGE = TCP_DYNAMIC_PORT_END - TCP_DYNAMIC_PORT_START; + +const ETH_HEADER_SIZE = 14; +const ETH_PAYLOAD_OFFSET = ETH_HEADER_SIZE; +const ETH_PAYLOAD_SIZE = 1500; +const ETH_TRAILER_SIZE = 4; +const ETH_FRAME_SIZE = ETH_HEADER_SIZE + ETH_PAYLOAD_SIZE + ETH_TRAILER_SIZE; +const IPV4_HEADER_SIZE = 20; +const IPV4_PAYLOAD_OFFSET = ETH_PAYLOAD_OFFSET + IPV4_HEADER_SIZE; +const IPV4_PAYLOAD_SIZE = ETH_PAYLOAD_SIZE - IPV4_HEADER_SIZE; +const UDP_HEADER_SIZE = 8; +const UDP_PAYLOAD_OFFSET = IPV4_PAYLOAD_OFFSET + UDP_HEADER_SIZE; +const UDP_PAYLOAD_SIZE = IPV4_PAYLOAD_SIZE - UDP_HEADER_SIZE; +const TCP_HEADER_SIZE = 20; +const TCP_PAYLOAD_OFFSET = IPV4_PAYLOAD_OFFSET + TCP_HEADER_SIZE; +const TCP_PAYLOAD_SIZE = IPV4_PAYLOAD_SIZE - TCP_HEADER_SIZE; +const ICMP_HEADER_SIZE = 4; + +const DEFAULT_DOH_SERVER = "cloudflare-dns.com"; function a2ethaddr(bytes) { return [0,1,2,3,4,5].map((i) => bytes[i].toString(16)).map(x => x.length === 1 ? "0" + x : x).join(":"); } -function siptolong(s) { - let parts = s.split(".").map(function(x) { return parseInt(x, 10); }); +function iptolong(parts) { return parts[0] << 24 | parts[1] << 16 | parts[2] << 8 | parts[3]; } -function iptolong(parts) { - return parts[0] << 24 | parts[1] << 16 | parts[2] << 8 | parts[3]; +class GrowableRingbuffer +{ + /** + * @param {number} initial_capacity + * @param {number} maximum_capacity + */ + constructor(initial_capacity, maximum_capacity) + { + initial_capacity = Math.min(initial_capacity, 16); + this.maximum_capacity = maximum_capacity ? Math.max(maximum_capacity, initial_capacity) : 0; + this.tail = 0; + this.head = 0; + this.length = 0; + this.buffer = new Uint8Array(initial_capacity); + } + + /** + * @param {Uint8Array} src_array + */ + write(src_array) + { + const src_length = src_array.length; + const total_length = this.length + src_length; + let capacity = this.buffer.length; + if(capacity < total_length) { + while(capacity < total_length) { + capacity *= 2; + } + if(this.maximum_capacity && capacity > this.maximum_capacity) { + throw new Error("stream capacity overflow in GrowableRingbuffer.write(), package dropped"); + } + const new_buffer = new Uint8Array(capacity); + this.peek(new_buffer); + this.tail = 0; + this.head = this.length; + this.buffer = new_buffer; + } + const buffer = this.buffer; + + const new_head = this.head + src_length; + if(new_head > capacity) { + const i_split = capacity - this.head; + buffer.set(src_array.subarray(0, i_split), this.head); + buffer.set(src_array.subarray(i_split)); + } + else { + buffer.set(src_array, this.head); + } + this.head = new_head % capacity; + this.length += src_length; + } + + /** + * @param {Uint8Array} dst_array + */ + peek(dst_array) + { + const length = Math.min(this.length, dst_array.length); + if(length) { + const buffer = this.buffer; + const capacity = buffer.length; + const new_tail = this.tail + length; + if(new_tail > capacity) { + const buf_len_left = new_tail % capacity; + const buf_len_right = capacity - this.tail; + dst_array.set(buffer.subarray(this.tail)); + dst_array.set(buffer.subarray(0, buf_len_left), buf_len_right); + } + else { + dst_array.set(buffer.subarray(this.tail, new_tail)); + } + } + return length; + } + + /** + * @param {number} length + */ + remove(length) + { + if(length > this.length) { + length = this.length; + } + if(length) { + this.tail = (this.tail + length) % this.buffer.length; + this.length -= length; + } + return length; + } } -function handle_fake_tcp(packet, adapter) +function create_eth_encoder_buf() { - let reply = {}; - reply.eth = { ethertype: ETHERTYPE_IPV4, src: adapter.router_mac, dest: packet.eth.src }; - reply.ipv4 = { - proto: IPV4_PROTO_TCP, - src: packet.ipv4.dest, - dest: packet.ipv4.src + const eth_frame = new Uint8Array(ETH_FRAME_SIZE); + const buffer = eth_frame.buffer; + const offset = eth_frame.byteOffset; + return { + eth_frame: eth_frame, + eth_frame_view: new DataView(buffer), + eth_payload_view: new DataView(buffer, offset + ETH_PAYLOAD_OFFSET, ETH_PAYLOAD_SIZE), + ipv4_payload_view: new DataView(buffer, offset + IPV4_PAYLOAD_OFFSET, IPV4_PAYLOAD_SIZE), + udp_payload_view: new DataView(buffer, offset + UDP_PAYLOAD_OFFSET, UDP_PAYLOAD_SIZE), + text_encoder: new TextEncoder() }; +} - let tuple = [ - packet.ipv4.src.join("."), - packet.tcp.sport, - packet.ipv4.dest.join("."), - packet.tcp.dport - ].join(":"); +/** + * Copy given data array into view starting at offset, return number of bytes written. + * + * @param {number} offset + * @param {ArrayBuffer|ArrayBufferView} data + * @param {DataView} view + * @param {Object} out + */ +function view_set_array(offset, data, view, out) +{ + out.eth_frame.set(data, view.byteOffset + offset); + return data.length; +} + +/** + * UTF8-encode given string into view starting at offset, return number of bytes written. + * + * @param {number} offset + * @param {string} str + * @param {DataView} view + * @param {Object} out + */ +function view_set_string(offset, str, view, out) +{ + return out.text_encoder.encodeInto(str, out.eth_frame.subarray(view.byteOffset + offset)).written; +} + +/** + * Calculate internet checksum for view[0 : length] and return the 16-bit result. + * Source: RFC768 and RFC1071 (chapter 4.1). + * + * @param {number} length + * @param {number} checksum + * @param {DataView} view + * @param {Object} out + */ +function calc_inet_checksum(length, checksum, view, out) +{ + const uint16_end = view.byteOffset + (length & ~1); + const eth_frame = out.eth_frame; + for(let i = view.byteOffset; i < uint16_end; i += 2) { + checksum += eth_frame[i] << 8 | eth_frame[i+1]; + } + if(length & 1) { + checksum += eth_frame[uint16_end] << 8; + } + while(checksum >> 16) { + checksum = (checksum & 0xffff) + (checksum >> 16); + } + return ~checksum & 0xffff; +} +/** + * @param {Object} out + * @param {Object} spec + */ +function make_packet(out, spec) +{ + dbg_assert(spec.eth); + out.eth_frame.fill(0); + return out.eth_frame.subarray(0, write_eth(spec, out)); +} + +function handle_fake_tcp(packet, adapter) +{ + const tuple = `${packet.ipv4.src.join(".")}:${packet.tcp.sport}:${packet.ipv4.dest.join(".")}:${packet.tcp.dport}`; if(packet.tcp.syn) { if(adapter.tcp_conn[tuple]) { dbg_log("SYN to already opened port", LOG_FETCH); } - if(adapter.on_tcp_connection(adapter, packet, tuple)) return; + if(adapter.on_tcp_connection(packet, tuple)) { + return; + } } if(!adapter.tcp_conn[tuple]) { - dbg_log(`I dont know about ${tuple}, so restting`, LOG_FETCH); + dbg_log(`I dont know about ${tuple}, so resetting`, LOG_FETCH); let bop = packet.tcp.ackn; if(packet.tcp.fin || packet.tcp.syn) bop += 1; + let reply = {}; + reply.eth = { ethertype: ETHERTYPE_IPV4, src: adapter.router_mac, dest: packet.eth.src }; + reply.ipv4 = { + proto: IPV4_PROTO_TCP, + src: packet.ipv4.dest, + dest: packet.ipv4.src + }; reply.tcp = { sport: packet.tcp.dport, dport: packet.tcp.sport, @@ -81,14 +268,14 @@ function handle_fake_tcp(packet, adapter) rst: true, ack: packet.tcp.syn }; - adapter.receive(make_packet(reply)); + adapter.receive(make_packet(adapter.eth_encoder_buf, reply)); return true; } adapter.tcp_conn[tuple].process(packet); } -function handle_fake_dns(packet, adapter) +function handle_fake_dns_static(packet, adapter) { let reply = {}; reply.eth = { ethertype: ETHERTYPE_IPV4, src: adapter.router_mac, dest: packet.eth.src }; @@ -108,7 +295,7 @@ function handle_fake_dns(packet, adapter) let q = packet.dns.questions[i]; switch(q.type){ - case 1: // A recrod + case 1: // A record answers.push({ name: q.name, type: q.type, @@ -127,10 +314,52 @@ function handle_fake_dns(packet, adapter) questions: packet.dns.questions, answers: answers }; - adapter.receive(make_packet(reply)); + adapter.receive(make_packet(adapter.eth_encoder_buf, reply)); return true; } +function handle_fake_dns_doh(packet, adapter) +{ + const fetch_url = `https://${adapter.doh_server || DEFAULT_DOH_SERVER}/dns-query`; + const fetch_opts = { + method: "POST", + headers: [["content-type", "application/dns-message"]], + body: packet.udp.data + }; + const preferred_fetch = (window.anura?.net?.fetch) || fetch; + preferred_fetch(fetch_url, fetch_opts).then(async (resp) => { + const reply = { + eth: { + ethertype: ETHERTYPE_IPV4, + src: adapter.router_mac, + dest: packet.eth.src + }, + ipv4: { + proto: IPV4_PROTO_UDP, + src: adapter.router_ip, + dest: packet.ipv4.src + }, + udp: { + sport: 53, + dport: packet.udp.sport, + data: new Uint8Array(await resp.arrayBuffer()) + } + }; + adapter.receive(make_packet(adapter.eth_encoder_buf, reply)); + }); + return true; +} + +function handle_fake_dns(packet, adapter) +{ + if(adapter.dns_method === "static") { + return handle_fake_dns_static(packet, adapter); + } + else { + return handle_fake_dns_doh(packet, adapter); + } +} + function handle_fake_ntp(packet, adapter) { let now = Date.now(); // - 1000 * 60 * 60 * 24 * 7; let now_n = now + NTP_EPOC_DIFF; @@ -158,7 +387,7 @@ function handle_fake_ntp(packet, adapter) { reply.ntp.trans_ts_f = now_n_f; reply.ntp.stratum = 2; - adapter.receive(make_packet(reply)); + adapter.receive(make_packet(adapter.eth_encoder_buf, reply)); return true; } @@ -213,40 +442,37 @@ function handle_fake_dhcp(packet, adapter) { options.push(new Uint8Array([255, 0])); reply.dhcp.options = options; - adapter.receive(make_packet(reply)); - + adapter.receive(make_packet(adapter.eth_encoder_buf, reply)); } function handle_fake_networking(data, adapter) { let packet = {}; parse_eth(data, packet); - if(packet.tcp) { - if(handle_fake_tcp(packet, adapter)) return true; - } - - if(packet.arp && packet.arp.oper === 1 && packet.arp.ptype === ETHERTYPE_IPV4) { - arp_whohas(packet, adapter); - } - - if(packet.dns) { - if(handle_fake_dns(packet, adapter)) return; - } - - if(packet.ntp) { - if(handle_fake_ntp(packet, adapter)) return; - } - - // ICMP Ping - if(packet.icmp && packet.icmp.type === 8) { - handle_fake_ping(packet, adapter); - } - if(packet.dhcp) { - if(handle_fake_dhcp(packet, adapter)) return; + if(packet.ipv4) { + if(packet.tcp) { + handle_fake_tcp(packet, adapter); + } + else if(packet.udp) { + if(packet.dns) { + handle_fake_dns(packet, adapter); + } + else if(packet.dhcp) { + handle_fake_dhcp(packet, adapter); + } + else if(packet.ntp) { + handle_fake_ntp(packet, adapter); + } + else if(packet.udp.dport === 8) { + handle_udp_echo(packet, adapter); + } + } + else if(packet.icmp && packet.icmp.type === 8) { + handle_fake_ping(packet, adapter); + } } - - if(packet.udp && packet.udp.dport === 8) { - handle_udp_echo(packet, adapter); + else if(packet.arp && packet.arp.oper === 1 && packet.arp.ptype === ETHERTYPE_IPV4) { + arp_whohas(packet, adapter); } } @@ -264,8 +490,8 @@ function parse_eth(data, o) { o.eth = eth; - // Remove CRC from the end of the packet maybe? - let payload = data.subarray(14, data.length); + // TODO: Remove CRC from the end of the packet maybe? + let payload = data.subarray(ETH_HEADER_SIZE, data.length); if(ethertype === ETHERTYPE_IPV4) { parse_ipv4(payload, o); @@ -281,18 +507,17 @@ function parse_eth(data, o) { } } -function write_eth(spec, data) { - let view = new DataView(data.buffer, data.byteOffset, data.byteLength); +function write_eth(spec, out) { + const view = out.eth_frame_view; + view_set_array(0, spec.eth.dest, view, out); + view_set_array(6, spec.eth.src, view, out); view.setUint16(12, spec.eth.ethertype); - for(let i = 0; i < 6; ++i ) view.setUint8(0 + i, spec.eth.dest[i]); - for(let i = 0; i < 6; ++i ) view.setUint8(6 + i, spec.eth.src[i]); - - let len = 14; + let len = ETH_HEADER_SIZE; if(spec.arp) { - len += write_arp(spec, data.subarray(14)); + len += write_arp(spec, out); } - if(spec.ipv4) { - len += write_ipv4(spec, data.subarray(14)); + else if(spec.ipv4) { + len += write_ipv4(spec, out); } return len; } @@ -314,26 +539,18 @@ function parse_arp(data, o) { o.arp = arp; } -function write_arp(spec, data) { - let view = new DataView(data.buffer, data.byteOffset, data.byteLength); +function write_arp(spec, out) { + const view = out.eth_payload_view; view.setUint16(0, spec.arp.htype); view.setUint16(2, spec.arp.ptype); view.setUint8(4, spec.arp.sha.length); view.setUint8(5, spec.arp.spa.length); view.setUint16(6, spec.arp.oper); - - for(let i = 0; i < 6; ++i) { - view.setUint8(8 + i, spec.arp.sha[i]); - view.setUint8(18 + i, spec.arp.tha[i]); - } - - for(let i = 0; i < 4; ++i) { - view.setUint8(14 + i, spec.arp.spa[i]); - view.setUint8(24 + i, spec.arp.tpa[i]); - } - + view_set_array(8, spec.arp.sha, view, out); + view_set_array(14, spec.arp.spa, view, out); + view_set_array(18, spec.arp.tha, view, out); + view_set_array(24, spec.arp.tpa, view, out); return 28; - } function parse_ipv4(data, o) { @@ -362,46 +579,37 @@ function parse_ipv4(data, o) { }; // Ethernet minmum packet size. - if(Math.max(len, 46) !== data.length) dbg_log(`ipv4 Length mismatch: ${len} != ${data.length}`, LOG_FETCH); + if(Math.max(len, 46) !== data.length) { + dbg_log(`ipv4 Length mismatch: ${len} != ${data.length}`, LOG_FETCH); + } o.ipv4 = ipv4; let ipdata = data.subarray(ihl * 4, len); if(proto === IPV4_PROTO_ICMP) { parse_icmp(ipdata, o); } - if(proto === IPV4_PROTO_TCP) { + else if(proto === IPV4_PROTO_TCP) { parse_tcp(ipdata, o); } - if(proto === IPV4_PROTO_UDP) { + else if(proto === IPV4_PROTO_UDP) { parse_udp(ipdata, o); } - - - return true; } -function write_ipv4(spec, data) { - let view = new DataView(data.buffer, data.byteOffset, data.byteLength); - - let ihl = 5; // 20 byte header length normally - let version = 4; - let len = 4 * ihl; // Total Length +function write_ipv4(spec, out) { + const view = out.eth_payload_view; + const ihl = IPV4_HEADER_SIZE >> 2; // header length in 32-bit words + const version = 4; + let len = IPV4_HEADER_SIZE; if(spec.icmp) { - len += write_icmp(spec, data.subarray(ihl * 4)); - } - if(spec.udp) { - len += write_udp(spec, data.subarray(ihl * 4)); + len += write_icmp(spec, out); } - if(spec.tcp) { - len += write_tcp(spec, data.subarray(ihl * 4)); + else if(spec.udp) { + len += write_udp(spec, out); } - if(spec.tcp_data) { - // TODO(perf) - for(let i = 0; i < spec.tcp_data.length; ++i) { - view.setUint8(len + i, spec.tcp_data[i]); - } - len += spec.tcp_data.length; + else if(spec.tcp) { + len += write_tcp(spec, out); } view.setUint8(0, version << 4 | (ihl & 0x0F)); @@ -411,24 +619,10 @@ function write_ipv4(spec, data) { view.setUint8(6, 2 << 5); // DF Flag view.setUint8(8, spec.ipv4.ttl || 32); view.setUint8(9, spec.ipv4.proto); - view.setUint16(10, 0); // Checksum is zero during hashing - - for(let i = 0; i < 4; ++i) { - view.setUint8(12 + i, spec.ipv4.src[i]); - view.setUint8(16 + i, spec.ipv4.dest[i]); - } - - let checksum = 0; - for(let i = 0; i < ihl * 2; ++i) { - // TODO(perf) - checksum += view.getUint16(i << 1); - if(checksum > 0xFFFF) { - checksum = (checksum & 0xFFFF) + 1; - } - } - - view.setUint16(10, checksum ^ 0xFFFF); - + view.setUint16(10, 0); // checksum initially zero before calculation + view_set_array(12, spec.ipv4.src, view, out); + view_set_array(16, spec.ipv4.dest, view, out); + view.setUint16(10, calc_inet_checksum(IPV4_HEADER_SIZE, 0, view, out)); return len; } @@ -440,33 +634,18 @@ function parse_icmp(data, o) { checksum: view.getUint16(2), data: data.subarray(4) }; - o.icmp = icmp; - return true; } -function write_icmp(spec, data) { - let view = new DataView(data.buffer, data.byteOffset, data.byteLength); +function write_icmp(spec, out) { + const view = out.ipv4_payload_view; view.setUint8(0, spec.icmp.type); view.setUint8(1, spec.icmp.code); - view.setUint16(2, 0); // checksum 0 during calc - - for(let i = 0; i < spec.icmp.data.length; ++i) { - view.setUint8(i + 4, spec.icmp.data[i]); - } - - let checksum = 0; - for(let i = 0; i < 4 + spec.icmp.data.length; i += 2) { - // TODO(perf) - checksum += view.getUint16(i); - if(checksum > 0xFFFF) { - checksum = (checksum & 0xFFFF) + 1; - } - } - - view.setUint16(2, checksum ^ 0xFFFF); - - return 4 + spec.icmp.data.length; + view.setUint16(2, 0); // checksum initially zero before calculation + const data_length = view_set_array(ICMP_HEADER_SIZE, spec.icmp.data, view, out); + const total_length = ICMP_HEADER_SIZE + data_length; + view.setUint16(2, calc_inet_checksum(total_length, 0, view, out)); + return total_length; } function parse_udp(data, o) { @@ -484,41 +663,45 @@ function parse_udp(data, o) { if(udp.dport === 67 || udp.sport === 67) { //DHCP parse_dhcp(data.subarray(8), o); } - if(udp.dport === 53 || udp.sport === 53) { + else if(udp.dport === 53 || udp.sport === 53) { parse_dns(data.subarray(8), o); } - if(udp.dport === 123) { + else if(udp.dport === 123) { parse_ntp(data.subarray(8), o); } o.udp = udp; - return true; } -function write_udp(spec, data) { - let view = new DataView(data.buffer, data.byteOffset, data.byteLength); - - let payload_length; - +function write_udp(spec, out) { + const view = out.ipv4_payload_view; + let total_length = UDP_HEADER_SIZE; if(spec.dhcp) { - payload_length = write_dhcp(spec, data.subarray(8)); - } else if(spec.dns) { - payload_length = write_dns(spec, data.subarray(8)); - } else if(spec.ntp) { - payload_length = write_ntp(spec, data.subarray(8)); - } else { - let raw_data = spec.udp.data; - payload_length = raw_data.length; - for(let i = 0; i < raw_data.length; ++i) { - view.setUint8(8+i, raw_data[i]); - } + total_length += write_dhcp(spec, out); + } + else if(spec.dns) { + total_length += write_dns(spec, out); + } + else if(spec.ntp) { + total_length += write_ntp(spec, out); + } + else { + total_length += view_set_array(0, spec.udp.data, out.udp_payload_view, out); } view.setUint16(0, spec.udp.sport); view.setUint16(2, spec.udp.dport); - view.setUint16(4, 8 + payload_length); - view.setUint16(6, 0); // Checksum - - return 8 + payload_length; + view.setUint16(4, total_length); + view.setUint16(6, 0); // checksum initially zero before calculation + + const pseudo_header = + (spec.ipv4.src[0] << 8 | spec.ipv4.src[1]) + + (spec.ipv4.src[2] << 8 | spec.ipv4.src[3]) + + (spec.ipv4.dest[0] << 8 | spec.ipv4.dest[1]) + + (spec.ipv4.dest[2] << 8 | spec.ipv4.dest[3]) + + IPV4_PROTO_UDP + + total_length; + view.setUint16(6, calc_inet_checksum(total_length, pseudo_header, view, out)); + return total_length; } function parse_dns(data, o) { @@ -572,8 +755,8 @@ function parse_dns(data, o) { o.dns = dns; } -function write_dns(spec, data) { - let view = new DataView(data.buffer, data.byteOffset, data.byteLength); +function write_dns(spec, out) { + const view = out.udp_payload_view; view.setUint16(0, spec.dns.id); view.setUint16(2, spec.dns.flags); view.setUint16(4, spec.dns.questions.length); @@ -583,12 +766,9 @@ function write_dns(spec, data) { for(let i = 0; i < spec.dns.questions.length; ++i) { let q = spec.dns.questions[i]; for(let s of q.name) { - view.setUint8(offset, s.length); - offset++; - for( let ii = 0; ii < s.length; ++ii) { - view.setUint8(offset, s.charCodeAt(ii)); - offset++; - } + const n_written = view_set_string(offset + 1, s, view, out); + view.setUint8(offset, n_written); + offset += 1 + n_written; } view.setUint16(offset, q.type); offset += 2; @@ -598,12 +778,9 @@ function write_dns(spec, data) { function write_reply(a) { for(let s of a.name) { - view.setUint8(offset, s.length); - offset++; - for( let ii = 0; ii < s.length; ++ii) { - view.setUint8(offset, s.charCodeAt(ii)); - offset++; - } + const n_written = view_set_string(offset + 1, s, view, out); + view.setUint8(offset, n_written); + offset += 1 + n_written; } view.setUint16(offset, a.type); offset += 2; @@ -613,12 +790,7 @@ function write_dns(spec, data) { offset += 4; view.setUint16(offset, a.data.length); offset += 2; - - for(let ii = 0; ii < a.data.length; ++ii) { - view.setUint8(offset + ii, a.data[ii]); - } - - offset += a.data.length; + offset += view_set_array(offset, a.data, view, out); } for(let i = 0; i < spec.dns.answers.length; ++i) { @@ -662,12 +834,10 @@ function parse_dhcp(data, o) { o.dhcp = dhcp; o.dhcp_options = dhcp.options; - return true; } -function write_dhcp(spec, data) { - let view = new DataView(data.buffer, data.byteOffset, data.byteLength); - +function write_dhcp(spec, out) { + const view = out.udp_payload_view; view.setUint8(0, spec.dhcp.op); view.setUint8(1, spec.dhcp.htype); view.setUint8(2, spec.dhcp.hlen); @@ -679,21 +849,14 @@ function write_dhcp(spec, data) { view.setUint32(16, spec.dhcp.yiaddr); view.setUint32(20, spec.dhcp.siaddr); view.setUint32(24, spec.dhcp.giaddr); - - for(let i = 0; i < spec.dhcp.chaddr.length; ++i) { - view.setUint8(28+i, spec.dhcp.chaddr[i]); - } + view_set_array(28, spec.dhcp.chaddr, view, out); view.setUint32(236, DHCP_MAGIC_COOKIE); let offset = 240; for(let o of spec.dhcp.options) { - for(let i = 0; i < o.length; ++i) { - view.setUint8(offset, o[i]); - ++offset; - } + offset += view_set_array(offset, o, view, out); } - return offset; } @@ -716,12 +879,10 @@ function parse_ntp(data, o) { trans_ts_i: view.getUint32(40), trans_ts_f: view.getUint32(44), }; - return true; } -function write_ntp(spec, data) { - let view = new DataView(data.buffer, data.byteOffset, data.byteLength); - +function write_ntp(spec, out) { + const view = out.udp_payload_view; view.setUint8(0, spec.ntp.flags); view.setUint8(1, spec.ntp.stratum); view.setUint8(2, spec.ntp.poll); @@ -737,12 +898,12 @@ function write_ntp(spec, data) { view.setUint32(36, spec.ntp.rec_ts_f); view.setUint32(40, spec.ntp.trans_ts_i); view.setUint32(44, spec.ntp.trans_ts_f); - return 48; } function parse_tcp(data, o) { let view = new DataView(data.buffer, data.byteOffset, data.byteLength); + let tcp = { sport: view.getUint16(0), dport: view.getUint16(2), @@ -769,12 +930,10 @@ function parse_tcp(data, o) { let offset = tcp.doff * 4; o.tcp_data = data.subarray(offset); - return true; } -function write_tcp(spec, data) { - let view = new DataView(data.buffer, data.byteOffset, data.byteLength); - +function write_tcp(spec, out) { + const view = out.ipv4_payload_view; let flags = 0; let tcp = spec.tcp; @@ -787,7 +946,7 @@ function write_tcp(spec, data) { if(tcp.ece) flags |= 0x40; if(tcp.cwr) flags |= 0x80; - let doff = 5; + const doff = TCP_HEADER_SIZE >> 2; // header length in 32-bit words view.setUint16(0, tcp.sport); view.setUint16(2, tcp.dport); @@ -796,98 +955,23 @@ function write_tcp(spec, data) { view.setUint8(12, doff << 4); view.setUint8(13, flags); view.setUint16(14, tcp.winsize); - view.setUint16(16, 0); // Checksum is 0 during calculation + view.setUint16(16, 0); // checksum initially zero before calculation view.setUint16(18, tcp.urgent || 0); - let total_len = (doff * 4) + (spec.tcp_data ? spec.tcp_data.length : 0); - - let checksum = 0; - let psudo_header = new Uint8Array(12); - let phview = new DataView(psudo_header.buffer, psudo_header.byteOffset, psudo_header.byteLength); - for(let i = 0; i < 4; ++i) { - phview.setUint8(i, spec.ipv4.src[i]); - phview.setUint8(4 + i, spec.ipv4.dest[i]); - } - phview.setUint8(9, IPV4_PROTO_TCP); - phview.setUint16(10, total_len); - - for(let i = 0; i < 6; ++i) { - // TODO(perf) - checksum += phview.getUint16(i << 1); - if(checksum > 0xFFFF) { - checksum = (checksum & 0xFFFF) + 1; - } - } - for(let i = 0; i < doff * 2; ++i) { - checksum += view.getUint16(i << 1); - if(checksum > 0xFFFF) { - checksum = (checksum & 0xFFFF) + 1; - } - } - + let total_length = TCP_HEADER_SIZE; if(spec.tcp_data) { - for(let i = 0; i < spec.tcp_data.length; i += 2) { - checksum += spec.tcp_data[i] << 8 | spec.tcp_data[i+1]; - if(checksum > 0xFFFF) { - checksum = (checksum & 0xFFFF) + 1; - } - } + total_length += view_set_array(TCP_HEADER_SIZE, spec.tcp_data, view, out); } - view.setUint16(16, checksum ^ 0xFFFF); - return doff * 4; -} - -function make_packet(spec) { - // TODO: Can we reuse this buffer? - let bytes = new Uint8Array(1518); // Max ethernet packet size - dbg_assert(spec.eth); - - let written = write_eth(spec, bytes); - return bytes.subarray(0, written); -} - -function fake_tcp_connect(dport, adapter) -{ - // TODO: check port collisions - let sport = 49152 + Math.floor(Math.random() * 1000); - let tuple = [ - adapter.vm_ip.join("."), - dport, - adapter.router_ip.join("."), - sport - ].join(":"); - - let reader; - let connector; - - let conn = new TCPConnection(); - conn.net = adapter; - conn.on_data = function(data) { if(reader) reader.call(handle, data); }; - conn.on_connect = function() { if(connector) connector.call(handle); }; - conn.tuple = tuple; - - conn.hsrc = adapter.router_mac; - conn.psrc = adapter.router_ip; - conn.sport = sport; - conn.hdest = adapter.vm_mac; - conn.dport = dport; - conn.pdest = adapter.vm_ip; - - adapter.tcp_conn[tuple] = conn; - conn.connect(); - - // TODO: Real event source - let handle = { - write: function(data) { conn.write(data); }, - on: function(event, cb) { - if( event === "data" ) reader = cb; - if( event === "connect" ) connector = cb; - }, - close: function() { conn.close(); } - }; - - return handle; + const pseudo_header = + (spec.ipv4.src[0] << 8 | spec.ipv4.src[1]) + + (spec.ipv4.src[2] << 8 | spec.ipv4.src[3]) + + (spec.ipv4.dest[0] << 8 | spec.ipv4.dest[1]) + + (spec.ipv4.dest[2] << 8 | spec.ipv4.dest[3]) + + IPV4_PROTO_TCP + + total_length; + view.setUint16(16, calc_inet_checksum(total_length, pseudo_header, view, out)); + return total_length; } /** @@ -895,8 +979,12 @@ function fake_tcp_connect(dport, adapter) */ function TCPConnection() { - this.send_buffer = new Uint8Array([]); - this.seq_history = []; + this.state = TCP_STATE_CLOSED; + this.send_buffer = new GrowableRingbuffer(2048, 0); + this.send_chunk_buf = new Uint8Array(TCP_PAYLOAD_SIZE); + this.in_active_close = false; + this.delayed_send_fin = false; + this.delayed_state = undefined; } TCPConnection.prototype.ipv4_reply = function() { @@ -918,7 +1006,28 @@ TCPConnection.prototype.ipv4_reply = function() { return reply; }; +TCPConnection.prototype.packet_reply = function(packet, tcp_options) { + const reply_tcp = { + sport: packet.tcp.dport, + dport: packet.tcp.sport, + winsize: packet.tcp.winsize, + ackn: this.ack, + seq: this.seq + }; + if(tcp_options) { + for(const opt in tcp_options) { + reply_tcp[opt] = tcp_options[opt]; + } + } + const reply = this.ipv4_reply(); + reply.tcp = reply_tcp; + return reply; +}; + +/* +// TODO: Is this method used anywhere anymore? It used to be called from fake_tcp_connect() which was removed. TCPConnection.prototype.connect = function() { + // dbg_log(`TCP[${this.tuple}]: connect(): sending SYN+ACK in state "${this.state}", next "${TCP_STATE_SYN_SENT}"`, LOG_FETCH); this.seq = 1338; this.ack = 1; this.start_seq = 0; @@ -935,8 +1044,9 @@ TCPConnection.prototype.connect = function() { winsize: 0, syn: true, }; - this.net.receive(make_packet(reply)); + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); }; +*/ TCPConnection.prototype.accept = function(packet) { this.seq = 1338; @@ -951,7 +1061,6 @@ TCPConnection.prototype.accept = function(packet) { this.winsize = packet.tcp.winsize; let reply = this.ipv4_reply(); - reply.tcp = { sport: this.sport, dport: this.dport, @@ -961,87 +1070,155 @@ TCPConnection.prototype.accept = function(packet) { syn: true, ack: true }; - this.net.receive(make_packet(reply)); + // dbg_log(`TCP[${this.tuple}]: accept(): sending SYN+ACK in state "${this.state}", next "${TCP_STATE_ESTABLISHED}"`, LOG_FETCH); + this.state = TCP_STATE_ESTABLISHED; + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); }; TCPConnection.prototype.process = function(packet) { - - // Receive Handshake Part 2, Send Part 3 - if(packet.tcp.syn) { - dbg_assert(packet.tcp.ack); - dbg_assert(this.state === TCP_STATE_SYN_SENT); - - this.ack = packet.tcp.seq + 1; - this.start_seq = packet.tcp.seq; - this.last_received_ackn = packet.tcp.ackn; - - let reply = this.ipv4_reply(); - this.net.receive(make_packet(reply)); - - this.state = TCP_STATE_ESTABLISHED; - if(this.on_connect) this.on_connect.call(this); + if(this.state === TCP_STATE_CLOSED) { + // dbg_log(`TCP[${this.tuple}]: WARNING: connection already closed, packet dropped`, LOG_FETCH); + const reply = this.packet_reply(packet, {rst: true}); + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); return; } - - if(packet.tcp.fin) { - dbg_log(`All done with ${this.tuple} resetting`, LOG_FETCH); - if(this.ack !== packet.tcp.seq) { - dbg_log("Closing the connecton, but seq was wrong", LOG_FETCH); - ++this.ack; // FIN increases seq# - } - let reply = this.ipv4_reply(); - reply.tcp = { - sport: packet.tcp.dport, - dport: packet.tcp.sport, - seq: this.seq, - ackn: this.ack, - winsize: packet.tcp.winsize, - rst: true, - }; - delete this.net.tcp_conn[this.tuple]; - this.net.receive(make_packet(reply)); + else if(packet.tcp.rst) { + // dbg_log(`TCP[${this.tuple}]: received RST in state "${this.state}"`, LOG_FETCH); + this.on_close(); + this.release(); return; } - - if(this.ack !== packet.tcp.seq) { - dbg_log(`Packet seq was wrong ex: ${this.ack} ~${this.ack - this.start_seq} pk: ${packet.tcp.seq} ~${this.start_seq - packet.tcp.seq} (${this.ack - packet.tcp.seq}) = ${this.name}`, LOG_FETCH); - - let reply = this.ipv4_reply(); - reply.tcp = { - sport: packet.tcp.dport, - dport: packet.tcp.sport, - seq: this.seq, - ackn: this.ack, - winsize: packet.tcp.winsize, - ack: true - }; - this.net.receive(make_packet(reply)); - + else if(packet.tcp.syn) { + if(this.state === TCP_STATE_SYN_SENT && packet.tcp.ack) { + this.ack = packet.tcp.seq + 1; + this.start_seq = packet.tcp.seq; + this.last_received_ackn = packet.tcp.ackn; + + const reply = this.ipv4_reply(); + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); + // dbg_log(`TCP[${this.tuple}]: received SYN+ACK in state "${this.state}", next "${TCP_STATE_ESTABLISHED}"`, LOG_FETCH); + this.state = TCP_STATE_ESTABLISHED; + if(this.on_connect) { + this.on_connect.call(this); + } + } + else { + dbg_log(`TCP[${this.tuple}]: WARNING: unexpected SYN packet dropped`, LOG_FETCH); + } + if(packet.tcp_data.length) { + dbg_log(`TCP[${this.tuple}]: WARNING: ${packet.tcp_data.length} bytes of unexpected SYN packet payload dropped`, LOG_FETCH); + } return; } - this.seq_history.push(`${packet.tcp.seq - this.start_seq}:${packet.tcp.seq + packet.tcp_data.length- this.start_seq}`); - - this.ack += packet.tcp_data.length; - - if(packet.tcp_data.length > 0) { - let reply = this.ipv4_reply(); - this.net.receive(make_packet(reply)); + if(packet.tcp.ack) { + if(this.state === TCP_STATE_SYN_RECEIVED) { + // dbg_log(`TCP[${this.tuple}]: received ACK in state "${this.state}", next "${TCP_STATE_ESTABLISHED}"`, LOG_FETCH); + this.state = TCP_STATE_ESTABLISHED; + } + else if(this.state === TCP_STATE_FIN_WAIT_1) { + if(!packet.tcp.fin) { // handle FIN+ACK in FIN_WAIT_1 separately further down below + // dbg_log(`TCP[${this.tuple}]: received ACK in state "${this.state}", next "${TCP_STATE_FIN_WAIT_2}"`, LOG_FETCH); + this.state = TCP_STATE_FIN_WAIT_2; + } + } + else if(this.state === TCP_STATE_CLOSING || this.state === TCP_STATE_LAST_ACK) { + // dbg_log(`TCP[${this.tuple}]: received ACK in state "${this.state}"`, LOG_FETCH); + this.release(); + return; + } } - if(this.last_received_ackn === undefined) this.last_received_ackn = packet.tcp.ackn; - let nread = packet.tcp.ackn - this.last_received_ackn; - //console.log("Read ", nread, "(", this.last_received_ackn, ") ", packet.tcp.ackn, packet.tcp.winsize) - if(nread > 0) { + if(this.last_received_ackn === undefined) { this.last_received_ackn = packet.tcp.ackn; - this.send_buffer = this.send_buffer.subarray(nread); - this.seq += nread; - this.pending = false; + } + else { + const n_ack = packet.tcp.ackn - this.last_received_ackn; + //console.log("Read ", n_ack, "(", this.last_received_ackn, ") ", packet.tcp.ackn, packet.tcp.winsize) + if(n_ack > 0) { + this.last_received_ackn = packet.tcp.ackn; + this.send_buffer.remove(n_ack); + this.seq += n_ack; + this.pending = false; + + if(this.delayed_send_fin && !this.send_buffer.length) { + // dbg_log(`TCP[${this.tuple}]: sending delayed FIN from active close in state "${this.state}", next "${this.delayed_state}"`, LOG_FETCH); + this.delayed_send_fin = false; + this.state = this.delayed_state; + const reply = this.ipv4_reply(); + reply.tcp.fin = true; + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); + return; + } + } + else if(n_ack < 0) { // TODO: could this just be a 32-bit sequence number overflow? + dbg_log(`TCP[${this.tuple}]: ERROR: ack underflow (pkt=${packet.tcp.ackn} last=${this.last_received_ackn}), resetting`, LOG_FETCH); + const reply = this.packet_reply(packet, {rst: true}); + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); + this.on_close(); + this.release(); + return; + } } - if(nread < 0) return; + if(packet.tcp.fin) { + if(this.ack !== packet.tcp.seq) { + dbg_log(`TCP[${this.tuple}]: WARNING: closing connection in state "${this.state}" with invalid seq (${this.ack} != ${packet.tcp.seq})`, LOG_FETCH); + } + ++this.ack; // FIN increases seqnr + const reply = this.packet_reply(packet, {}); + if(this.state === TCP_STATE_ESTABLISHED) { + // dbg_log(`TCP[${this.tuple}]: received FIN in state "${this.state}, next "${TCP_STATE_CLOSE_WAIT}""`, LOG_FETCH); + reply.tcp.ack = true; + this.state = TCP_STATE_CLOSE_WAIT; + this.on_shutdown(); + } + else if(this.state === TCP_STATE_FIN_WAIT_1) { + if(packet.tcp.ack) { + // dbg_log(`TCP[${this.tuple}]: received ACK+FIN in state "${this.state}"`, LOG_FETCH); + this.release(); + } + else { + // dbg_log(`TCP[${this.tuple}]: received ACK in state "${this.state}", next "${TCP_STATE_CLOSING}"`, LOG_FETCH); + this.state = TCP_STATE_CLOSING; + } + reply.tcp.ack = true; + } + else if(this.state === TCP_STATE_FIN_WAIT_2) { + // dbg_log(`TCP[${this.tuple}]: received FIN in state "${this.state}"`, LOG_FETCH); + this.release(); + reply.tcp.ack = true; + } + else { + // dbg_log(`TCP[${this.tuple}]: ERROR: received FIN in unexpected TCP state "${this.state}", resetting`, LOG_FETCH); + this.release(); + this.on_close(); + reply.tcp.rst = true; + } + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); + } + else if(this.ack !== packet.tcp.seq) { + // Handle TCP Keep-Alives silently. + // Excerpt from RFC 9293, 3.8.4. TCP Keep-Alives: + // To confirm that an idle connection is still active, these + // implementations send a probe segment designed to elicit a response + // from the TCP peer. Such a segment generally contains SEG.SEQ = + // SND.NXT-1 and may or may not contain one garbage octet of data. + if(this.ack !== packet.tcp.seq + 1) { + dbg_log(`Packet seq was wrong ex: ${this.ack} ~${this.ack - this.start_seq} ` + + `pk: ${packet.tcp.seq} ~${this.start_seq - packet.tcp.seq} ` + + `(${this.ack - packet.tcp.seq}) = ${this.name}`, LOG_FETCH); + } + const reply = this.packet_reply(packet, {ack: true}); + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); + } + else if(packet.tcp.ack && packet.tcp_data.length > 0) { + this.ack += packet.tcp_data.length; + const reply = this.ipv4_reply(); + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); + this.on_data(packet.tcp_data); + } - this.on_data(packet.tcp_data); this.pump(); }; @@ -1049,37 +1226,83 @@ TCPConnection.prototype.process = function(packet) { * @param {Uint8Array} data */ TCPConnection.prototype.write = function(data) { - if(this.send_buffer.length > 0) { - // TODO: Pretty inefficient - let concat = new Uint8Array(this.send_buffer.byteLength + data.byteLength); - concat.set(this.send_buffer, 0); - concat.set(data, this.send_buffer.byteLength); - this.send_buffer = concat; - } else { - this.send_buffer = data; + if(!this.in_active_close) { + this.send_buffer.write(data); + } + this.pump(); +}; + +/** + * @param {!Array} data_array + */ +TCPConnection.prototype.writev = function(data_array) { + if(!this.in_active_close) { + for(const data of data_array) { + this.send_buffer.write(data); + } } this.pump(); }; TCPConnection.prototype.close = function() { - this.state = TCP_STATE_FIN_WAIT_1; - let reply = this.ipv4_reply(); - reply.tcp.fin = true; - this.net.receive(make_packet(reply)); + if(!this.in_active_close) { + this.in_active_close = true; + let next_state; + if(this.state === TCP_STATE_ESTABLISHED || this.state === TCP_STATE_SYN_RECEIVED) { + next_state = TCP_STATE_FIN_WAIT_1; + } + else if(this.state === TCP_STATE_CLOSE_WAIT) { + next_state = TCP_STATE_LAST_ACK; + } + else { + if(this.state !== TCP_STATE_SYN_SENT) { + dbg_log(`TCP[${this.tuple}]: active close in unexpected state "${this.state}"`, LOG_FETCH); + } + this.release(); + return; + } + + if(this.send_buffer.length || this.pending) { + // dbg_log(`TCP[${this.tuple}]: active close, delaying FIN in state "${this.state}", delayed next "${next_state}"`, LOG_FETCH); + this.delayed_send_fin = true; + this.delayed_state = next_state; + } + else { + // dbg_log(`TCP[${this.tuple}]: active close, sending FIN in state "${this.state}", next "${next_state}"`, LOG_FETCH); + this.state = next_state; + const reply = this.ipv4_reply(); + reply.tcp.fin = true; + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); + } + } this.pump(); }; -TCPConnection.prototype.pump = function() { +TCPConnection.prototype.on_shutdown = function() { + // forward FIN event from guest device to network adapter +}; - if(this.send_buffer.length > 0 && !this.pending) { - let data = this.send_buffer.subarray(0, 500); - let reply = this.ipv4_reply(); +TCPConnection.prototype.on_close = function() { + // forward RST event from guest device to network adapter +}; - this.pending = true; - if(this.send_buffer.length < 1) reply.tcp.fin = true; +TCPConnection.prototype.release = function() { + if(this.net.tcp_conn[this.tuple]) { + // dbg_log(`TCP[${this.tuple}]: connection closed in state "${this.state}"`, LOG_FETCH); + this.state = TCP_STATE_CLOSED; + delete this.net.tcp_conn[this.tuple]; + } +}; + +TCPConnection.prototype.pump = function() { + if(this.send_buffer.length && !this.pending) { + const data = this.send_chunk_buf; + const n_ready = this.send_buffer.peek(data); + const reply = this.ipv4_reply(); reply.tcp.psh = true; - reply.tcp_data = data; - this.net.receive(make_packet(reply)); + reply.tcp_data = data.subarray(0, n_ready); + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); + this.pending = true; } }; @@ -1111,7 +1334,7 @@ function arp_whohas(packet, adapter) { tha: packet.eth.src, tpa: packet.arp.spa }; - adapter.receive(make_packet(reply)); + adapter.receive(make_packet(adapter.eth_encoder_buf, reply)); } function handle_fake_ping(packet, adapter) { @@ -1127,7 +1350,7 @@ function handle_fake_ping(packet, adapter) { code: packet.icmp.code, data: packet.icmp.data }; - adapter.receive(make_packet(reply)); + adapter.receive(make_packet(adapter.eth_encoder_buf, reply)); } function handle_udp_echo(packet, adapter) { @@ -1144,5 +1367,5 @@ function handle_udp_echo(packet, adapter) { dport: packet.udp.sport, data: new TextEncoder().encode(packet.udp.data_s) }; - adapter.receive(make_packet(reply)); + adapter.receive(make_packet(adapter.eth_encoder_buf, reply)); } diff --git a/src/browser/fetch_network.js b/src/browser/fetch_network.js index 489938942c..8d0b980aba 100644 --- a/src/browser/fetch_network.js +++ b/src/browser/fetch_network.js @@ -16,8 +16,10 @@ function FetchNetworkAdapter(bus, config) this.vm_ip = new Uint8Array((config.vm_ip || "192.168.86.100").split(".").map(function(x) { return parseInt(x, 10); })); this.masquerade = config.masquerade === undefined || !!config.masquerade; this.vm_mac = new Uint8Array(6); - + this.dns_method = config.dns_method || "static"; + this.doh_server = config.doh_server; this.tcp_conn = {}; + this.eth_encoder_buf = create_eth_encoder_buf(); // Ex: 'https://corsproxy.io/?' this.cors_proxy = config.cors_proxy; @@ -37,7 +39,7 @@ FetchNetworkAdapter.prototype.destroy = function() { }; -FetchNetworkAdapter.prototype.on_tcp_connection = function(adapter, packet, tuple) +FetchNetworkAdapter.prototype.on_tcp_connection = function(packet, tuple) { if(packet.tcp.dport === 80) { let conn = new TCPConnection(); @@ -46,7 +48,7 @@ FetchNetworkAdapter.prototype.on_tcp_connection = function(adapter, packet, tupl conn.on_data = on_data_http; conn.tuple = tuple; conn.accept(packet); - adapter.tcp_conn[tuple] = conn; + this.tcp_conn[tuple] = conn; return true; } return false; @@ -54,11 +56,10 @@ FetchNetworkAdapter.prototype.on_tcp_connection = function(adapter, packet, tupl /** * @this {TCPConnection} - * @param {ArrayBuffer} data + * @param {!ArrayBuffer} data */ async function on_data_http(data) { - if(!data) return; // Make type checking happy. this.read = this.read || ""; this.read += new TextDecoder().decode(data); if(this.read && this.read.indexOf("\r\n\r\n") !== -1) { @@ -83,11 +84,14 @@ async function on_data_http(data) let req_headers = new Headers(); for(let i = 1; i < headers.length; ++i) { - let parts = headers[i].split(": "); - let key = parts[0].toLowerCase(); - let value = parts[1]; - if( key === "host" ) target.host = value; - else if( key.length > 1 ) req_headers.set(parts[0], value); + const header = this.net.parse_http_header(headers[i]); + if(!header) { + console.warn('The request contains an invalid header: "%s"', headers[i]); + this.write(new TextEncoder().encode("HTTP/1.1 400 Bad Request\r\nContent-Length: 0")); + return; + } + if( header.key.toLowerCase() === "host" ) target.host = header.value; + else req_headers.append(header.key, header.value); } dbg_log("HTTP Dispatch: " + target.href, LOG_FETCH); @@ -99,6 +103,7 @@ async function on_data_http(data) if(["put", "post"].indexOf(opts.method.toLowerCase()) !== -1) { opts.body = data; } +/* const [resp, ab] = await this.net.fetch(target.href, opts); const lines = [ `HTTP/1.1 ${resp.status} ${resp.statusText}`, @@ -122,6 +127,54 @@ async function on_data_http(data) this.write(new TextEncoder().encode(lines.join("\r\n"))); this.write(new Uint8Array(ab)); + this.close(); +*/ + const fetch_url = this.net.cors_proxy ? this.net.cors_proxy + encodeURIComponent(target.href) : target.href; + const encoder = new TextEncoder(); + let response_started = false; + fetch(fetch_url, opts).then((resp) => { + const header_lines = [ + `HTTP/1.1 ${resp.status} ${resp.statusText}`, + `x-was-fetch-redirected: ${!!resp.redirected}`, + `x-fetch-resp-url: ${resp.url}`, + "Connection: closed" + ]; + for(const [key, value] of resp.headers.entries()) { + if(!["content-encoding", "connection", "content-length", "transfer-encoding"].includes(key.toLowerCase())) { + header_lines.push(`${key}: ${value}`); + } + } + this.write(encoder.encode(header_lines.join("\r\n") + "\r\n\r\n")); + response_started = true; + + const resp_reader = resp.body.getReader(); + const pump = ({ value, done }) => { + if(value) { + this.write(value); + } + if(done) { + this.close(); + } + else { + return resp_reader.read().then(pump); + } + }; + resp_reader.read().then(pump); + }) + .catch((e) => { + console.warn("Fetch Failed: " + fetch_url + "\n" + e); + if(!response_started) { + const body = encoder.encode(`Fetch ${fetch_url} failed:\n\n${e.stack || e.message}`); + const header_lines = [ + "HTTP/1.1 502 Fetch Error", + "Content-Type: text/plain", + `Content-Length: ${body.length}`, + "Connection: closed" + ]; + this.writev([encoder.encode(header_lines.join("\r\n") + "\r\n\r\n"), body]); + } + this.close(); + }); } } @@ -151,6 +204,41 @@ FetchNetworkAdapter.prototype.fetch = async function(url, options) } }; +FetchNetworkAdapter.prototype.parse_http_header = function(header) +{ + const parts = header.match(/^([^:]*):(.*)$/); + if(!parts) { + dbg_log("Unable to parse HTTP header", LOG_FETCH); + return; + } + + const key = parts[1]; + const value = parts[2].trim(); + + if(key.length === 0) + { + dbg_log("Header key is empty, raw header", LOG_FETCH); + return; + } + if(value.length === 0) + { + dbg_log("Header value is empty", LOG_FETCH); + return; + } + if(!/^[\w-]+$/.test(key)) + { + dbg_log("Header key contains forbidden characters", LOG_FETCH); + return; + } + if(!/^[\x20-\x7E]+$/.test(value)) + { + dbg_log("Header value contains forbidden characters", LOG_FETCH); + return; + } + + return { key, value }; +}; + /** * @param {Uint8Array} data */ @@ -159,12 +247,6 @@ FetchNetworkAdapter.prototype.send = function(data) handle_fake_networking(data, this); }; - -FetchNetworkAdapter.prototype.tcp_connect = function(dport) -{ - return fake_tcp_connect(dport, this); -}; - /** * @param {Uint8Array} data */ diff --git a/src/browser/main.js b/src/browser/main.js index bdabbb2256..150e4fb98c 100644 --- a/src/browser/main.js +++ b/src/browser/main.js @@ -11,7 +11,7 @@ function set_title(text) { - document.title = text + " - Virtual x86" + (DEBUG ? " - debug" : ""); + document.title = text + " - v86" + (DEBUG ? " - debug" : ""); const description = document.querySelector("meta[name=description]"); description && (description.content = "Running " + text); } @@ -145,7 +145,7 @@ name: "Arch Linux", memory_size: 512 * 1024 * 1024, vga_memory_size: 8 * 1024 * 1024, - state: { url: host + "arch_state.bin.zst" }, + state: { url: host + "arch_state-v2.bin.zst" }, filesystem: { baseurl: host + "arch/", }, @@ -217,14 +217,14 @@ id: "redox", name: "Redox", hda: { - url: host + "redox_demo_i686_2022-11-26_643_harddrive/.img", - size: 512 * 1024 * 1024, + url: host + "redox_demo_i686_2024-09-07_1225_harddrive/.img", + size: 671088640, async: true, fixed_chunk_size: 1024 * 1024, use_parts: true, }, - memory_size: 512 * 1024 * 1024, - state: { url: host + "redox_state.bin.zst" }, + memory_size: 1024 * 1024 * 1024, + state: { url: host + "redox_state-v2.bin.zst" }, homepage: "https://www.redox-os.org/", acpi: true, }, @@ -232,13 +232,13 @@ id: "redox-boot", name: "Redox", hda: { - url: host + "redox_demo_i686_2022-11-26_643_harddrive/.img", - size: 512 * 1024 * 1024, + url: host + "redox_demo_i686_2024-09-07_1225_harddrive/.img", + size: 671088640, async: true, fixed_chunk_size: 1024 * 1024, use_parts: true, }, - memory_size: 512 * 1024 * 1024, + memory_size: 1024 * 1024 * 1024, homepage: "https://www.redox-os.org/", acpi: true, }, @@ -259,7 +259,7 @@ id: "fiwix", memory_size: 256 * 1024 * 1024, hda: { - url: host + "FiwixOS-3.3-i386/.img", + url: host + "FiwixOS-3.4-i386/.img", size: 1024 * 1024 * 1024, async: true, fixed_chunk_size: 1024 * 1024, @@ -272,13 +272,13 @@ id: "haiku", memory_size: 512 * 1024 * 1024, hda: { - url: host + "haiku-v3/.img", + url: host + "haiku-v4/.img", size: 1 * 1024 * 1024 * 1024, async: true, fixed_chunk_size: 1024 * 1024, use_parts: true, }, - state: { url: host + "haiku_state-v3.bin.zst" }, + state: { url: host + "haiku_state-v4.bin.zst" }, name: "Haiku", homepage: "https://www.haiku-os.org/", }, @@ -286,7 +286,7 @@ id: "haiku-boot", memory_size: 512 * 1024 * 1024, hda: { - url: host + "haiku-v3/.img", + url: host + "haiku-v4/.img", size: 1 * 1024 * 1024 * 1024, async: true, fixed_chunk_size: 1024 * 1024, @@ -295,14 +295,36 @@ name: "Haiku", homepage: "https://www.haiku-os.org/", }, + { + id: "beos", + memory_size: 512 * 1024 * 1024, + hda: { + url: host + "beos5/.img", + size: 536870912, + async: true, + fixed_chunk_size: 1024 * 1024, + use_parts: true, + }, + name: "BeOS 5", + }, { id: "msdos", hda: { - url: host + "msdos.img", - size: 8 * 1024 * 1024, - async: false, + url: host + "msdos622/.img", + size: 64 * 1024 * 1024, + async: true, + fixed_chunk_size: 256 * 1024, + use_parts: true, + }, + name: "MS-DOS 6.22", + }, + { + id: "msdos4", + fda: { + url: host + "msdos4.img", + size: 1474560, }, - name: "MS-DOS", + name: "MS-DOS 4", }, { id: "freedos", @@ -323,6 +345,15 @@ }, name: "Freedos with FreeGEM", }, + { + id: "xcom", + fda: { + url: host + "xcom144.img", + size: 1440 * 1024, + }, + name: "Freedos with Xcom", + homepage: "http://xcom.infora.hu/index.html", + }, { id: "psychdos", hda: { @@ -359,7 +390,7 @@ url: host + "windows101.img", size: 1474560, }, - name: "Windows", + name: "Windows 1.01", }, { id: "windows2", @@ -416,7 +447,7 @@ size: 10068480, async: false, }, - name: "Buildroot Linux", + name: "Buildroot Linux 6.8", filesystem: {}, cmdline: "tsc=reliable mitigations=off random.trust_cpu=on", }, @@ -447,6 +478,7 @@ async: false, }, name: "ELKS", + homepage: "https://github.com/ghaerr/elks", }, { id: "nodeos", @@ -493,6 +525,17 @@ }, homepage: "https://www.minix3.org/", }, + { + id: "unix-v7", + name: "Unix V7", + hda: { + url: host + "unix-v7x86-0.8a/.img", + size: 152764416, + async: true, + fixed_chunk_size: 256 * 1024, + use_parts: true, + }, + }, { id: "kolibrios", fda: { @@ -624,6 +667,15 @@ name: "bootLogo", homepage: "https://github.com/nanochess/bootLogo", }, + { + id: "pillman", + fda: { + url: host + "pillman.img", + size: 512, + }, + name: "Pillman", + homepage: "https://github.com/nanochess/Pillman", + }, { id: "sectorlisp", fda: { @@ -731,6 +783,19 @@ }, name: "Windows 2000", }, + { + id: "windows-me", + memory_size: 128 * 1024 * 1024, + hda: { + url: host + "windowsme/.img", + size: 834666496, + async: true, + fixed_chunk_size: 256 * 1024, + use_parts: true, + }, + state: { url: host + "windows-me_state.bin.zst" }, + name: "Windows ME", + }, { id: "windowsnt4", memory_size: 512 * 1024 * 1024, @@ -847,6 +912,16 @@ name: "Tilck", homepage: "https://github.com/vvaltchev/tilck", }, + { + id: "littlekernel", + multiboot: { + url: host + "littlekernel-multiboot.img", + async: false, + size: 969580, + }, + name: "Little Kernel", + homepage: "https://github.com/littlekernel/lk", + }, { id: "sanos", memory_size: 128 * 1024 * 1024, @@ -855,7 +930,7 @@ async: false, size: 1474560, }, - name: "sanos", + name: "Sanos", homepage: "http://www.jbox.dk/sanos/", }, { @@ -887,28 +962,30 @@ id: "reactos", memory_size: 512 * 1024 * 1024, hda: { - url: host + "reactos/.img", - size: 500 * 1024 * 1024, + url: host + "reactos-v2/.img", + size: 681574400, async: true, fixed_chunk_size: 1024 * 1024, use_parts: true, }, - state: { url: host + "reactos_state.bin.zst" }, + state: { url: host + "reactos_state-v2.bin.zst" }, mac_address_translation: true, name: "ReactOS", + acpi: true, homepage: "https://reactos.org/", }, { id: "reactos-boot", memory_size: 512 * 1024 * 1024, hda: { - url: host + "reactos/.img", - size: 500 * 1024 * 1024, + url: host + "reactos-v2/.img", + size: 681574400, async: true, fixed_chunk_size: 1024 * 1024, use_parts: true, }, name: "ReactOS", + acpi: true, homepage: "https://reactos.org/", }, { @@ -1006,7 +1083,7 @@ fixed_chunk_size: 1024 * 1024, use_parts: true, }, - name: "Android", + name: "Android 4", }, { id: "tinycore", @@ -1325,9 +1402,12 @@ { element.onclick = e => { - e.preventDefault(); - element.blur(); - start_emulation(os, null); + if(!e.ctrlKey) + { + e.preventDefault(); + element.blur(); + start_emulation(os, null); + } }; } } @@ -1383,6 +1463,85 @@ start_emulation(profile, query_args); }); } + + const os_info = Array.from(document.querySelectorAll("#oses tbody tr")).map(element => + { + const [_, size_raw, unit] = element.children[1].textContent.match(/([\d\.]+)\+? (\w+)/); + let size = +size_raw; + if(unit === "MB") size *= 1024 * 1024; + else if(unit === "KB") size *= 1024; + return { + element, + size, + graphical: element.children[2].firstChild.className === "gui_icon", + family: element.children[3].textContent.replace(/-like/, ""), + arch: element.children[4].textContent, + status: element.children[5].textContent, + source: element.children[6].textContent, + languages: new Set(element.children[7].textContent.split(", ")), + medium: element.children[8].textContent, + }; + }); + + const filter_elements = document.querySelectorAll("#filter input"); + for(const element of filter_elements) + { + element.onchange = update_filters; + } + + function update_filters() + { + const filter = {}; + for(const element of filter_elements) + { + filter[element.id.replace(/filter_/, "")] = element.checked; + } + + const show_all = !Object.values(filter).includes(true); + for(const os of os_info) + { + const show = show_all || + filter["graphical"] && os.graphical || + filter["text"] && !os.graphical || + filter["linux"] && os.family === "Linux" || + filter["bsd"] && os.family === "BSD" || + filter["windows"] && os.family === "Windows" || + filter["unix"] && os.family === "Unix" || + filter["dos"] && os.family === "DOS" || + filter["custom"] && os.family === "Custom" || + filter["floppy"] && os.medium === "Floppy" || + filter["cd"] && os.medium === "CD" || + filter["hd"] && os.medium === "HD" || + filter["modern"] && os.status === "Modern" || + filter["historic"] && os.status === "Historic" || + filter["opensource"] && os.source === "Open-source" || + filter["proprietary"] && os.source === "Proprietary" || + filter["bootsector"] && os.size <= 512 || + filter["lt5mb"] && os.size <= 5 * 1024 * 1024 || + filter["gt5mb"] && os.size > 5 * 1024 * 1024 || + filter["16bit"] && os.arch === "16-bit" || + filter["32bit"] && os.arch === "32-bit" || + filter["asm"] && os.languages.has("ASM") || + filter["c"] && os.languages.has("C") || + filter["cpp"] && os.languages.has("C++") || + filter["other_lang"] && ["Java", "Haskell", "Rust", "Erlang", "Oberon"].some(l => os.languages.has(l)); + + os.element.style.display = show ? "" : "none"; + } + } + + function set_proxy_value(id, value) + { + const elem = $(id); + if(elem) + { + elem.onclick = () => $("relay_url").value = value; + } + } + set_proxy_value("network_none", ""); + set_proxy_value("network_fetch", "fetch"); + set_proxy_value("network_relay", "wss://relay.widgetry.org/"); + set_proxy_value("network_wisp", "wisps://wisp.mercurywork.shop/v86/"); } function debug_onload() @@ -1471,7 +1630,8 @@ { $("boot_options").style.display = "none"; - const new_query_args = new URLSearchParams({ "profile": profile?.id || "custom" }); + const new_query_args = new Map(); + new_query_args.set("profile", profile?.id || "custom"); const settings = {}; @@ -1571,7 +1731,7 @@ settings.vga_memory_size = vram * 1024 * 1024; } - settings.acpi = query_args.has("acpi") ? bool_arg(query_args.get("acpi")) : undefined; + settings.acpi = query_args.has("acpi") ? bool_arg(query_args.get("acpi")) : settings.acpi; settings.use_bochs_bios = query_args.get("bios") === "bochs"; settings.net_device_type = query_args.get("net_device_type") === "virtio" ? "virtio" : "ne2k"; } @@ -1584,10 +1744,15 @@ if(!settings.relay_url) { settings.relay_url = $("relay_url").value; - if(!DEFAULT_NETWORKING_PROXIES.includes(settings.relay_url)) new_query_args.append("relay_url", settings.relay_url); + if(!DEFAULT_NETWORKING_PROXIES.includes(settings.relay_url)) new_query_args.set("relay_url", settings.relay_url); + } + if(settings.relay_url.startsWith("fetch:")) + { + settings.cors_proxy = settings.relay_url.slice(6); + settings.relay_url = "fetch"; } settings.disable_audio = $("disable_audio").checked || settings.disable_audio; - if(settings.disable_audio) new_query_args.append("mute", "1"); + if(settings.disable_audio) new_query_args.set("mute", "1"); // some settings cannot be overridden when a state image is used if(!settings.initial_state) @@ -1646,26 +1811,26 @@ { settings.memory_size = memory_size * MB; } - if(memory_size !== DEFAULT_MEMORY_SIZE) new_query_args.append("m", String(memory_size)); + if(memory_size !== DEFAULT_MEMORY_SIZE) new_query_args.set("m", String(memory_size)); const vga_memory_size = parseInt($("vga_memory_size").value, 10) || DEFAULT_VGA_MEMORY_SIZE; if(!settings.vga_memory_size || vga_memory_size !== DEFAULT_VGA_MEMORY_SIZE) { settings.vga_memory_size = vga_memory_size * MB; } - if(vga_memory_size !== DEFAULT_VGA_MEMORY_SIZE) new_query_args.append("vram", String(vga_memory_size)); + if(vga_memory_size !== DEFAULT_VGA_MEMORY_SIZE) new_query_args.set("vram", String(vga_memory_size)); const boot_order = parseInt($("boot_order").value, 16) || DEFAULT_BOOT_ORDER; if(!settings.boot_order || boot_order !== DEFAULT_BOOT_ORDER) { settings.boot_order = boot_order; } - if(settings.boot_order !== DEFAULT_BOOT_ORDER) new_query_args.append("boot_order", String(settings.boot_order)); + if(settings.boot_order !== DEFAULT_BOOT_ORDER) new_query_args.set("boot_order", String(settings.boot_order)); if(settings.acpi === undefined) { settings.acpi = $("acpi").checked; - if(settings.acpi) new_query_args.append("acpi", "1"); + if(settings.acpi) new_query_args.set("acpi", "1"); } if(!settings.bios) @@ -1701,6 +1866,7 @@ net_device: { type: settings.net_device_type || "ne2k", relay_url: settings.relay_url, + cors_proxy: settings.cors_proxy }, autostart: true, @@ -1759,7 +1925,7 @@ }, CLEAR_STATS ? 5000 : 1000); } - if(["dsl", "helenos", "android", "android4"].includes(profile?.id)) + if(["dsl", "helenos", "android", "android4", "redox", "beos"].includes(profile?.id)) { setTimeout(() => { // hack: Start automatically @@ -1767,7 +1933,7 @@ }, 3000); } - init_ui(settings, emulator); + init_ui(profile, settings, emulator); if(query_args?.has("c")) { @@ -1803,7 +1969,7 @@ * @param {Object} settings * @param {V86} emulator */ - function init_ui(settings, emulator) + function init_ui(profile, settings, emulator) { $("loading").style.display = "none"; $("runtime_options").style.display = "block"; @@ -1840,7 +2006,7 @@ $("exit").onclick = function() { - emulator.stop(); + emulator.destroy(); location.href = location.pathname; }; @@ -2042,7 +2208,7 @@ { var elem = $("get_" + type + "_image"); - if(!obj || obj.size > 100 * 1024 * 1024) + if(!obj || obj.async) { elem.style.display = "none"; return; @@ -2050,7 +2216,7 @@ elem.onclick = function(e) { - const filename = buffer.file && buffer.file.name || (settings.id + (type === "cdrom" ? ".iso" : ".img")); + const filename = buffer.file && buffer.file.name || ((profile?.id || "v86") + (type === "cdrom" ? ".iso" : ".img")); if(buffer.get_as_file) { @@ -2269,6 +2435,11 @@ $("screen_container").onclick = function() { + if(emulator.is_running() && emulator.speaker_adapter && emulator.speaker_adapter.audio_context.state === "suspended") + { + emulator.speaker_adapter.audio_context.resume(); + } + if(mouse_is_enabled && os_uses_mouse) { emulator.lock_mouse(); @@ -2473,7 +2644,7 @@ { if(window.history.pushState) { - const search = "?" + params.toString(); + let search = "?" + params.entries().map(([key, value]) => key + "=" + value.replace(/[?&=#+]/g, encodeURIComponent))["toArray"]().join("&"); window.history.pushState({ search }, "", search); } } diff --git a/src/browser/network.js b/src/browser/network.js index 7972c7eb41..22d58f26bb 100644 --- a/src/browser/network.js +++ b/src/browser/network.js @@ -25,6 +25,7 @@ function NetworkAdapter(url, bus, id) this.reconnect_interval = 10000; this.last_connect_attempt = Date.now() - this.reconnect_interval; this.send_queue_limit = 64; + this.destroyed = false; this.bus.register("net" + this.id + "-send", function(data) { @@ -44,8 +45,11 @@ NetworkAdapter.prototype.handle_close = function(e) { //console.log("onclose", e); - this.connect(); - setTimeout(this.connect.bind(this), this.reconnect_interval); + if(!this.destroyed) + { + this.connect(); + setTimeout(this.connect.bind(this), this.reconnect_interval); + } }; NetworkAdapter.prototype.handle_open = function(e) @@ -67,6 +71,7 @@ NetworkAdapter.prototype.handle_error = function(e) NetworkAdapter.prototype.destroy = function() { + this.destroyed = true; if(this.socket) { this.socket.close(); diff --git a/src/browser/screen.js b/src/browser/screen.js index 176ae429e3..d207d443b3 100644 --- a/src/browser/screen.js +++ b/src/browser/screen.js @@ -12,6 +12,22 @@ function ScreenAdapter(options, screen_fill_buffer) console.assert(screen_container, "options.container must be provided"); + const MODE_TEXT = 0; + const MODE_GRAPHICAL = 1; + const MODE_GRAPHICAL_TEXT = 2; + + const CHARACTER_INDEX = 0; + const FLAGS_INDEX = 1; + const BG_COLOR_INDEX = 2; + const FG_COLOR_INDEX = 3; + const TEXT_BUF_COMPONENT_SIZE = 4; + + const FLAG_BLINKING = 0x01; + const FLAG_FONT_PAGE_B = 0x02; + + this.FLAG_BLINKING = FLAG_BLINKING; + this.FLAG_FONT_PAGE_B = FLAG_FONT_PAGE_B; + var graphic_screen = screen_container.getElementsByTagName("canvas")[0], graphic_context = graphic_screen.getContext("2d", { alpha: false }), @@ -27,20 +43,20 @@ function ScreenAdapter(options, screen_fill_buffer) cursor_col, /** @type {number} */ - scale_x = 1, + scale_x = options.scale !== undefined ? options.scale : 1, /** @type {number} */ - scale_y = 1, + scale_y = options.scale !== undefined ? options.scale : 1, base_scale = 1, changed_rows, - // are we in graphical mode now? - is_graphical = !!options.use_graphical_text, + // current display mode: MODE_GRAPHICAL or either MODE_TEXT/MODE_GRAPHICAL_TEXT + mode, // Index 0: ASCII code - // Index 1: Blinking + // Index 1: Flags bitset (see FLAG_...) // Index 2: Background color // Index 3: Foreground color text_mode_data, @@ -49,16 +65,40 @@ function ScreenAdapter(options, screen_fill_buffer) text_mode_width, // number of rows - text_mode_height; - - const CHARACTER_INDEX = 0; - const BLINKING_INDEX = 1; - const BG_COLOR_INDEX = 2; - const FG_COLOR_INDEX = 3; - const TEXT_MODE_COMPONENT_SIZE = 4; - - var stopped = false; - var paused = false; + text_mode_height, + + // graphical text mode's offscreen canvas contexts + offscreen_context, + offscreen_extra_context, + + // fonts + font_context, + font_image_data, + font_is_visible = new Int8Array(8 * 256), + font_height, + font_width, + font_width_9px, + font_width_dbl, + font_copy_8th_col, + font_page_a = 0, + font_page_b = 0, + + // blink state + blink_visible, + tm_last_update = 0, + + // cursor attributes + cursor_start, + cursor_end, + cursor_enabled, + + // 8-bit Unicode character maps + charmap_default = [], + charmap = charmap_default, + + // render loop state + timer_id = 0, + paused = false; // 0x12345 -> "#012345" function number_as_color(n) @@ -67,86 +107,276 @@ function ScreenAdapter(options, screen_fill_buffer) return "#" + "0".repeat(6 - n.length) + n; } - - /** - * Charmaps that constraint unicode sequences for the default dospage - * @const - */ - var charmap_high = new Uint16Array([ - 0x2302, - 0xC7, 0xFC, 0xE9, 0xE2, 0xE4, 0xE0, 0xE5, 0xE7, - 0xEA, 0xEB, 0xE8, 0xEF, 0xEE, 0xEC, 0xC4, 0xC5, - 0xC9, 0xE6, 0xC6, 0xF4, 0xF6, 0xF2, 0xFB, 0xF9, - 0xFF, 0xD6, 0xDC, 0xA2, 0xA3, 0xA5, 0x20A7, 0x192, - 0xE1, 0xED, 0xF3, 0xFA, 0xF1, 0xD1, 0xAA, 0xBA, - 0xBF, 0x2310, 0xAC, 0xBD, 0xBC, 0xA1, 0xAB, 0xBB, - 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, - 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, - 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F, - 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, - 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, - 0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, - 0x3B1, 0xDF, 0x393, 0x3C0, 0x3A3, 0x3C3, 0xB5, 0x3C4, - 0x3A6, 0x398, 0x3A9, 0x3B4, 0x221E, 0x3C6, 0x3B5, 0x2229, - 0x2261, 0xB1, 0x2265, 0x2264, 0x2320, 0x2321, 0xF7, - 0x2248, 0xB0, 0x2219, 0xB7, 0x221A, 0x207F, 0xB2, 0x25A0, 0xA0 - ]); - - /** @const */ - var charmap_low = new Uint16Array([ - 0x20, 0x263A, 0x263B, 0x2665, 0x2666, 0x2663, 0x2660, 0x2022, - 0x25D8, 0x25CB, 0x25D9, 0x2642, 0x2640, 0x266A, 0x266B, 0x263C, - 0x25BA, 0x25C4, 0x2195, 0x203C, 0xB6, 0xA7, 0x25AC, 0x21A8, - 0x2191, 0x2193, 0x2192, 0x2190, 0x221F, 0x2194, 0x25B2, 0x25BC - ]); - - var charmap = [], - chr; - - for(var i = 0; i < 256; i++) - { - if(i > 126) - { - chr = charmap_high[i - 0x7F]; - } - else if(i < 32) - { - chr = charmap_low[i]; + function render_font_bitmap(vga_bitmap) + { + // - Browsers impose limts on the X- and Y-axes of bitmaps (typically around 8 to 32k). + // Draw the 8 VGA font pages of 256 glyphs in 8 rows of 256 columns, this results + // in 2048, 2304 or 4096px on the X-axis (for 8, 9 or 16px VGA font width, resp.). + // This 2d layout is also convenient for glyph lookup when rendering text. + // - Font bitmap pixels are black and either fully opaque (alpha 255) or fully transparent (0). + const bitmap_width = font_width * 256; + const bitmap_height = font_height * 8; + + let font_canvas = font_context ? font_context.canvas : null; + if(!font_canvas || font_canvas.width !== bitmap_width || font_canvas.height !== bitmap_height) + { + if(!font_canvas) + { + font_canvas = new OffscreenCanvas(bitmap_width, bitmap_height); + font_context = font_canvas.getContext("2d"); + } + else + { + font_canvas.width = bitmap_width; + font_canvas.height = bitmap_height; + } + font_image_data = font_context.createImageData(bitmap_width, bitmap_height); } - else - { - chr = i; + + const font_bitmap = font_image_data.data; + let i_dst = 0, is_visible; + const put_bit = font_width_dbl ? + function(value) + { + is_visible = is_visible || value; + font_bitmap[i_dst + 3] = value; + font_bitmap[i_dst + 7] = value; + i_dst += 8; + } : + function(value) + { + is_visible = is_visible || value; + font_bitmap[i_dst + 3] = value; + i_dst += 4; + }; + + // move i_vga from end of glyph to start of next glyph + const vga_inc_chr = 32 - font_height; + // move i_dst from end of font page (bitmap row) to start of next font page + const dst_inc_row = bitmap_width * (font_height - 1) * 4; + // move i_dst from end of glyph (bitmap column) to start of next glyph + const dst_inc_col = (font_width - bitmap_width * font_height) * 4; + // move i_dst from end of a glyph's scanline to start of its next scanline + const dst_inc_line = font_width * 255 * 4; + + for(let i_chr_all = 0, i_vga = 0; i_chr_all < 2048; ++i_chr_all, i_vga += vga_inc_chr, i_dst += dst_inc_col) + { + const i_chr = i_chr_all % 256; + if(i_chr_all && !i_chr) + { + i_dst += dst_inc_row; + } + is_visible = false; + for(let i_line = 0; i_line < font_height; ++i_line, ++i_vga, i_dst += dst_inc_line) + { + const line_bits = vga_bitmap[i_vga]; + for(let i_bit = 0x80; i_bit > 0; i_bit >>= 1) + { + put_bit(line_bits & i_bit ? 255 : 0); + } + if(font_width_9px) + { + put_bit(font_copy_8th_col && i_chr >= 0xC0 && i_chr <= 0xDF && line_bits & 1 ? 255 : 0); + } + } + font_is_visible[i_chr_all] = is_visible ? 1 : 0; } - charmap[i] = String.fromCharCode(chr); + font_context.putImageData(font_image_data, 0, 0); } - graphic_context.imageSmoothingEnabled = false; + function render_changed_rows() + { + const font_canvas = font_context.canvas; + const offscreen_extra_canvas = offscreen_extra_context.canvas; + const txt_row_size = text_mode_width * TEXT_BUF_COMPONENT_SIZE; + const gfx_width = text_mode_width * font_width; + const row_extra_1_y = 0; + const row_extra_2_y = font_height; + + let n_rows_rendered = 0; + for(let row_i = 0, row_y = 0, txt_i = 0; row_i < text_mode_height; ++row_i, row_y += font_height) + { + if(!changed_rows[row_i]) + { + txt_i += txt_row_size; + continue; + } + ++n_rows_rendered; - cursor_element.classList.add("cursor"); - cursor_element.style.position = "absolute"; - cursor_element.style.backgroundColor = "#ccc"; - cursor_element.style.width = "7px"; - cursor_element.style.display = "inline-block"; + // clear extra row 2 + offscreen_extra_context.clearRect(0, row_extra_2_y, gfx_width, font_height); - text_screen.style.display = "block"; - graphic_screen.style.display = "none"; + let fg_rgba, fg_x, bg_rgba, bg_x; + for(let col_x = 0; col_x < gfx_width; col_x += font_width, txt_i += TEXT_BUF_COMPONENT_SIZE) + { + const chr = text_mode_data[txt_i + CHARACTER_INDEX]; + const chr_flags = text_mode_data[txt_i + FLAGS_INDEX]; + const chr_bg_rgba = text_mode_data[txt_i + BG_COLOR_INDEX]; + const chr_fg_rgba = text_mode_data[txt_i + FG_COLOR_INDEX]; + const chr_font_page = chr_flags & FLAG_FONT_PAGE_B ? font_page_b : font_page_a; + const chr_visible = (!(chr_flags & FLAG_BLINKING) || blink_visible) && font_is_visible[(chr_font_page << 8) + chr]; + + if(bg_rgba !== chr_bg_rgba) + { + if(bg_rgba !== undefined) + { + // draw opaque block of background color into offscreen_context + offscreen_context.fillStyle = number_as_color(bg_rgba); + offscreen_context.fillRect(bg_x, row_y, col_x - bg_x, font_height); + } + bg_rgba = chr_bg_rgba; + bg_x = col_x; + } + + if(fg_rgba !== chr_fg_rgba) + { + if(fg_rgba !== undefined) + { + // draw opaque block of foreground color into extra row 1 + offscreen_extra_context.fillStyle = number_as_color(fg_rgba); + offscreen_extra_context.fillRect(fg_x, row_extra_1_y, col_x - fg_x, font_height); + } + fg_rgba = chr_fg_rgba; + fg_x = col_x; + } + + if(chr_visible) + { + // copy transparent glyphs into extra row 2 + offscreen_extra_context.drawImage(font_canvas, + chr * font_width, chr_font_page * font_height, font_width, font_height, + col_x, row_extra_2_y, font_width, font_height); + } + } + + // draw rightmost block of foreground color into extra row 1 + offscreen_extra_context.fillStyle = number_as_color(fg_rgba); + offscreen_extra_context.fillRect(fg_x, row_extra_1_y, gfx_width - fg_x, font_height); + + // combine extra row 1 (colors) and 2 (glyphs) into extra row 1 (colored glyphs) + offscreen_extra_context.globalCompositeOperation = "destination-in"; + offscreen_extra_context.drawImage(offscreen_extra_canvas, + 0, row_extra_2_y, gfx_width, font_height, + 0, row_extra_1_y, gfx_width, font_height); + offscreen_extra_context.globalCompositeOperation = "source-over"; + + // draw rightmost block of background color into offscreen_context + offscreen_context.fillStyle = number_as_color(bg_rgba); + offscreen_context.fillRect(bg_x, row_y, gfx_width - bg_x, font_height); + + // copy colored glyphs from extra row 1 into offscreen_context (on top of background colors) + offscreen_context.drawImage(offscreen_extra_canvas, + 0, row_extra_1_y, gfx_width, font_height, + 0, row_y, gfx_width, font_height); + } + + if(n_rows_rendered) + { + if(blink_visible && cursor_enabled && changed_rows[cursor_row]) + { + const cursor_txt_i = (cursor_row * text_mode_width + cursor_col) * TEXT_BUF_COMPONENT_SIZE; + const cursor_rgba = text_mode_data[cursor_txt_i + FG_COLOR_INDEX]; + offscreen_context.fillStyle = number_as_color(cursor_rgba); + offscreen_context.fillRect( + cursor_col * font_width, + cursor_row * font_height + cursor_start, + font_width, + cursor_end - cursor_start + 1); + } + changed_rows.fill(0); + } + + return n_rows_rendered; + } + + function mark_blinking_rows_dirty() + { + const txt_row_size = text_mode_width * TEXT_BUF_COMPONENT_SIZE; + for(let row_i = 0, txt_i = 0; row_i < text_mode_height; ++row_i) + { + if(changed_rows[row_i]) + { + txt_i += txt_row_size; + continue; + } + for(let col_i = 0; col_i < text_mode_width; ++col_i, txt_i += TEXT_BUF_COMPONENT_SIZE) + { + if(text_mode_data[txt_i + FLAGS_INDEX] & FLAG_BLINKING) + { + changed_rows[row_i] = 1; + txt_i += txt_row_size - col_i * TEXT_BUF_COMPONENT_SIZE; + break; + } + } + } + } this.init = function() { - // initialize with mode and size presets as expected by the bios - // to avoid flickering during early startup - this.set_mode(is_graphical); + // map 8-bit DOS codepage 437 character range 0-31 to 16-bit Unicode codepoints + const charmap_low = new Uint16Array([ + 0x20, 0x263A, 0x263B, 0x2665, 0x2666, 0x2663, 0x2660, 0x2022, + 0x25D8, 0x25CB, 0x25D9, 0x2642, 0x2640, 0x266A, 0x266B, 0x263C, + 0x25BA, 0x25C4, 0x2195, 0x203C, 0xB6, 0xA7, 0x25AC, 0x21A8, + 0x2191, 0x2193, 0x2192, 0x2190, 0x221F, 0x2194, 0x25B2, 0x25BC + ]); + // map 8-bit DOS codepage 437 character range 127-255 to 16-bit Unicode codepoints + const charmap_high = new Uint16Array([ + 0x2302, + 0xC7, 0xFC, 0xE9, 0xE2, 0xE4, 0xE0, 0xE5, 0xE7, + 0xEA, 0xEB, 0xE8, 0xEF, 0xEE, 0xEC, 0xC4, 0xC5, + 0xC9, 0xE6, 0xC6, 0xF4, 0xF6, 0xF2, 0xFB, 0xF9, + 0xFF, 0xD6, 0xDC, 0xA2, 0xA3, 0xA5, 0x20A7, 0x192, + 0xE1, 0xED, 0xF3, 0xFA, 0xF1, 0xD1, 0xAA, 0xBA, + 0xBF, 0x2310, 0xAC, 0xBD, 0xBC, 0xA1, 0xAB, 0xBB, + 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, + 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, + 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F, + 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, + 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, + 0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, + 0x3B1, 0xDF, 0x393, 0x3C0, 0x3A3, 0x3C3, 0xB5, 0x3C4, + 0x3A6, 0x398, 0x3A9, 0x3B4, 0x221E, 0x3C6, 0x3B5, 0x2229, + 0x2261, 0xB1, 0x2265, 0x2264, 0x2320, 0x2321, 0xF7, + 0x2248, 0xB0, 0x2219, 0xB7, 0x221A, 0x207F, 0xB2, 0x25A0, 0xA0 + ]); + + // initialize 8-bit DOS codepage 437 map charmap[256] (Uint8 -> String[1]) + for(var i = 0, chr; i < 256; i++) + { + if(i > 126) + { + chr = charmap_high[i - 0x7F]; + } + else if(i < 32) + { + chr = charmap_low[i]; + } + else + { + chr = i; + } + charmap_default.push(String.fromCharCode(chr)); + } - if(is_graphical) + // setup text mode cursor DOM element + cursor_element.classList.add("cursor"); + cursor_element.style.position = "absolute"; + cursor_element.style.backgroundColor = "#ccc"; + cursor_element.style.width = "7px"; + cursor_element.style.display = "inline-block"; + + // initialize display mode and size to 80x25 text with 9x16 font + this.set_mode(false); + this.set_size_text(80, 25); + if(mode === MODE_GRAPHICAL_TEXT) { - // assume 80x25 with 9x16 font this.set_size_graphical(720, 400, 720, 400); } - else - { - this.set_size_text(80, 25); - } + + // initialize CSS scaling + this.set_scale(scale_x, scale_y); this.timer(); }; @@ -155,7 +385,7 @@ function ScreenAdapter(options, screen_fill_buffer) { const image = new Image(); - if(is_graphical) + if(mode === MODE_GRAPHICAL || mode === MODE_GRAPHICAL_TEXT) { image.src = graphic_screen.toDataURL("image/png"); } @@ -176,7 +406,7 @@ function ScreenAdapter(options, screen_fill_buffer) { for(let x = 0; x < text_mode_width; x++) { - const index = (y * text_mode_width + x) * TEXT_MODE_COMPONENT_SIZE; + const index = (y * text_mode_width + x) * TEXT_BUF_COMPONENT_SIZE; const character = text_mode_data[index + CHARACTER_INDEX]; const bg_color = text_mode_data[index + BG_COLOR_INDEX]; const fg_color = text_mode_data[index + FG_COLOR_INDEX]; @@ -204,16 +434,16 @@ function ScreenAdapter(options, screen_fill_buffer) return image; }; - this.put_char = function(row, col, chr, blinking, bg_color, fg_color) + this.put_char = function(row, col, chr, flags, bg_color, fg_color) { dbg_assert(row >= 0 && row < text_mode_height); dbg_assert(col >= 0 && col < text_mode_width); dbg_assert(chr >= 0 && chr < 0x100); - const p = TEXT_MODE_COMPONENT_SIZE * (row * text_mode_width + col); + const p = TEXT_BUF_COMPONENT_SIZE * (row * text_mode_width + col); text_mode_data[p + CHARACTER_INDEX] = chr; - text_mode_data[p + BLINKING_INDEX] = blinking; + text_mode_data[p + FLAGS_INDEX] = flags; text_mode_data[p + BG_COLOR_INDEX] = bg_color; text_mode_data[p + FG_COLOR_INDEX] = fg_color; @@ -222,10 +452,27 @@ function ScreenAdapter(options, screen_fill_buffer) this.timer = function() { - if(!stopped) + timer_id = requestAnimationFrame(() => this.update_screen()); + }; + + this.update_screen = function() + { + if(!paused) { - requestAnimationFrame(() => is_graphical ? this.update_graphical() : this.update_text()); + if(mode === MODE_TEXT) + { + this.update_text(); + } + else if(mode === MODE_GRAPHICAL) + { + this.update_graphical(); + } + else + { + this.update_graphical_text(); + } } + this.timer(); }; this.update_text = function() @@ -238,22 +485,44 @@ function ScreenAdapter(options, screen_fill_buffer) changed_rows[i] = 0; } } - - this.timer(); }; this.update_graphical = function() { - if(!paused) + this.screen_fill_buffer(); + }; + + this.update_graphical_text = function() + { + if(offscreen_context) { - this.screen_fill_buffer(); + // toggle cursor and blinking character visibility at a frequency of ~3.75hz + const tm_now = performance.now(); + if(tm_now - tm_last_update > 266) + { + blink_visible = !blink_visible; + if(cursor_enabled) + { + changed_rows[cursor_row] = 1; + } + mark_blinking_rows_dirty(); + tm_last_update = tm_now; + } + // copy to DOM canvas only if anything new was rendered + if(render_changed_rows()) + { + graphic_context.drawImage(offscreen_context.canvas, 0, 0); + } } - this.timer(); }; this.destroy = function() { - stopped = true; + if(timer_id) + { + cancelAnimationFrame(timer_id); + timer_id = 0; + } }; this.pause = function() @@ -261,6 +530,7 @@ function ScreenAdapter(options, screen_fill_buffer) paused = true; cursor_element.classList.remove("blinking-cursor"); }; + this.continue = function() { paused = false; @@ -269,17 +539,57 @@ function ScreenAdapter(options, screen_fill_buffer) this.set_mode = function(graphical) { - is_graphical = graphical; + mode = graphical ? MODE_GRAPHICAL : (options.use_graphical_text ? MODE_GRAPHICAL_TEXT : MODE_TEXT); - if(graphical) + if(mode === MODE_TEXT) + { + text_screen.style.display = "block"; + graphic_screen.style.display = "none"; + } + else { text_screen.style.display = "none"; graphic_screen.style.display = "block"; + + if(mode === MODE_GRAPHICAL_TEXT && changed_rows) + { + changed_rows.fill(1); + } } - else + }; + + this.set_font_bitmap = function(height, width_9px, width_dbl, copy_8th_col, vga_bitmap, vga_bitmap_changed) + { + const width = width_dbl ? 16 : (width_9px ? 9 : 8); + if(font_height !== height || font_width !== width || font_width_9px !== width_9px || + font_width_dbl !== width_dbl || font_copy_8th_col !== copy_8th_col || + vga_bitmap_changed) + { + const size_changed = font_width !== width || font_height !== height; + font_height = height; + font_width = width; + font_width_9px = width_9px; + font_width_dbl = width_dbl; + font_copy_8th_col = copy_8th_col; + if(mode === MODE_GRAPHICAL_TEXT) + { + render_font_bitmap(vga_bitmap); + changed_rows.fill(1); + if(size_changed) + { + this.set_size_graphical_text(); + } + } + } + }; + + this.set_font_page = function(page_a, page_b) + { + if(font_page_a !== page_a || font_page_b !== page_b) { - text_screen.style.display = "block"; - graphic_screen.style.display = "none"; + font_page_a = page_a; + font_page_b = page_b; + changed_rows.fill(1); } }; @@ -289,6 +599,44 @@ function ScreenAdapter(options, screen_fill_buffer) graphic_context.fillRect(0, 0, graphic_screen.width, graphic_screen.height); }; + this.set_size_graphical_text = function() + { + if(!font_context) + { + return; + } + + const gfx_width = font_width * text_mode_width; + const gfx_height = font_height * text_mode_height; + const offscreen_extra_height = font_height * 2; + + if(!offscreen_context || offscreen_context.canvas.width !== gfx_width || + offscreen_context.canvas.height !== gfx_height || + offscreen_extra_context.canvas.height !== offscreen_extra_height) + { + // resize offscreen canvases + if(!offscreen_context) + { + const offscreen_canvas = new OffscreenCanvas(gfx_width, gfx_height); + offscreen_context = offscreen_canvas.getContext("2d", { alpha: false }); + const offscreen_extra_canvas = new OffscreenCanvas(gfx_width, offscreen_extra_height); + offscreen_extra_context = offscreen_extra_canvas.getContext("2d"); + } + else + { + offscreen_context.canvas.width = gfx_width; + offscreen_context.canvas.height = gfx_height; + offscreen_extra_context.canvas.width = gfx_width; + offscreen_extra_context.canvas.height = offscreen_extra_height; + } + + // resize DOM canvas graphic_screen + this.set_size_graphical(gfx_width, gfx_height, gfx_width, gfx_height); + + changed_rows.fill(1); + } + }; + /** * @param {number} cols * @param {number} rows @@ -301,27 +649,34 @@ function ScreenAdapter(options, screen_fill_buffer) } changed_rows = new Int8Array(rows); - text_mode_data = new Int32Array(cols * rows * TEXT_MODE_COMPONENT_SIZE); + text_mode_data = new Int32Array(cols * rows * TEXT_BUF_COMPONENT_SIZE); text_mode_width = cols; text_mode_height = rows; - while(text_screen.childNodes.length > rows) + if(mode === MODE_TEXT) { - text_screen.removeChild(text_screen.firstChild); - } + while(text_screen.childNodes.length > rows) + { + text_screen.removeChild(text_screen.firstChild); + } - while(text_screen.childNodes.length < rows) - { - text_screen.appendChild(document.createElement("div")); - } + while(text_screen.childNodes.length < rows) + { + text_screen.appendChild(document.createElement("div")); + } + + for(var i = 0; i < rows; i++) + { + this.text_update_row(i); + } - for(var i = 0; i < rows; i++) + update_scale_text(); + } + else if(mode === MODE_GRAPHICAL_TEXT) { - this.text_update_row(i); + this.set_size_graphical_text(); } - - update_scale_text(); }; this.set_size_graphical = function(width, height, buffer_width, buffer_height) @@ -339,12 +694,12 @@ function ScreenAdapter(options, screen_fill_buffer) graphic_screen.width = width; graphic_screen.height = height; - // graphic_context loses its configuration when its graphic_screen gets resized, reinitialize + + // graphic_context must be reconfigured whenever its graphic_screen is resized graphic_context.imageSmoothingEnabled = false; // add some scaling to tiny resolutions - if(!options.disable_autoscale && - width <= 640 && + if(width <= 640 && width * 2 < window.innerWidth * window.devicePixelRatio && height * 2 < window.innerHeight * window.devicePixelRatio) { @@ -358,6 +713,11 @@ function ScreenAdapter(options, screen_fill_buffer) update_scale_graphic(); }; + this.set_charmap = function(text_charmap) + { + charmap = text_charmap || charmap_default; + }; + this.set_scale = function(s_x, s_y) { scale_x = s_x; @@ -366,7 +726,6 @@ function ScreenAdapter(options, screen_fill_buffer) update_scale_text(); update_scale_graphic(); }; - this.set_scale(scale_x, scale_y); function update_scale_text() { @@ -380,6 +739,11 @@ function ScreenAdapter(options, screen_fill_buffer) function elem_set_scale(elem, scale_x, scale_y, use_scale) { + if(!scale_x || !scale_y) + { + return; + } + elem.style.width = ""; elem.style.height = ""; @@ -433,17 +797,34 @@ function ScreenAdapter(options, screen_fill_buffer) } } - this.update_cursor_scanline = function(start, end, visible) + this.update_cursor_scanline = function(start, end, enabled) { - if(visible) - { - cursor_element.style.display = "inline"; - cursor_element.style.height = (end - start) + "px"; - cursor_element.style.marginTop = start + "px"; - } - else + if(start !== cursor_start || end !== cursor_end || enabled !== cursor_enabled) { - cursor_element.style.display = "none"; + if(mode === MODE_TEXT) + { + if(enabled) + { + cursor_element.style.display = "inline"; + cursor_element.style.height = (end - start) + "px"; + cursor_element.style.marginTop = start + "px"; + } + else + { + cursor_element.style.display = "none"; + } + } + else if(mode === MODE_GRAPHICAL_TEXT) + { + if(cursor_row < text_mode_height) + { + changed_rows[cursor_row] = 1; + } + } + + cursor_start = start; + cursor_end = end; + cursor_enabled = enabled; } }; @@ -467,7 +848,7 @@ function ScreenAdapter(options, screen_fill_buffer) this.text_update_row = function(row) { - var offset = TEXT_MODE_COMPONENT_SIZE * row * text_mode_width, + var offset = TEXT_BUF_COMPONENT_SIZE * row * text_mode_width, row_element, color_element, fragment; @@ -484,7 +865,7 @@ function ScreenAdapter(options, screen_fill_buffer) { color_element = document.createElement("span"); - blinking = text_mode_data[offset + BLINKING_INDEX]; + blinking = text_mode_data[offset + FLAGS_INDEX] & FLAG_BLINKING; bg_color = text_mode_data[offset + BG_COLOR_INDEX]; fg_color = text_mode_data[offset + FG_COLOR_INDEX]; @@ -500,7 +881,7 @@ function ScreenAdapter(options, screen_fill_buffer) // put characters of the same color in one element while(i < text_mode_width && - text_mode_data[offset + BLINKING_INDEX] === blinking && + (text_mode_data[offset + FLAGS_INDEX] & FLAG_BLINKING) === blinking && text_mode_data[offset + BG_COLOR_INDEX] === bg_color && text_mode_data[offset + FG_COLOR_INDEX] === fg_color) { @@ -510,7 +891,7 @@ function ScreenAdapter(options, screen_fill_buffer) dbg_assert(charmap[ascii]); i++; - offset += TEXT_MODE_COMPONENT_SIZE; + offset += TEXT_BUF_COMPONENT_SIZE; if(row === cursor_row) { @@ -591,9 +972,9 @@ function ScreenAdapter(options, screen_fill_buffer) for(let x = 0; x < text_mode_width; x++) { - const index = (y * text_mode_width + x) * TEXT_MODE_COMPONENT_SIZE; + const index = (y * text_mode_width + x) * TEXT_BUF_COMPONENT_SIZE; const character = text_mode_data[index + CHARACTER_INDEX]; - result += String.fromCharCode(character); + result += charmap[character]; } return result; diff --git a/src/browser/starter.js b/src/browser/starter.js index 0283e4c05c..d2f1430947 100644 --- a/src/browser/starter.js +++ b/src/browser/starter.js @@ -58,7 +58,7 @@ * * - `screen Object` (No screen) - An object with the following properties: * - `container HTMLElement` - An HTMLElement, see above. - * - `disable_autoscale boolean` (false) - Disable automatic scaling of small resolutions. + * - `scale` (1) - Set initial scale_x and scale_y, if 0 disable automatic upscaling and dpi-adaption * * *** * @@ -101,7 +101,7 @@ disable_keyboard: (boolean|undefined), wasm_fn: (Function|undefined), screen: ({ - disable_autoscale: (boolean|undefined), + scale: (number|undefined), } | undefined), }} options * @constructor @@ -313,11 +313,11 @@ V86.prototype.continue_init = async function(emulator, options) // TODO: remove bus, use direct calls instead if(relay_url === "fetch") { - this.network_adapter = new FetchNetworkAdapter(this.bus); + this.network_adapter = new FetchNetworkAdapter(this.bus, options.net_device); } else if(relay_url.startsWith("wisp://") || relay_url.startsWith("wisps://")) { - this.network_adapter = new WispNetworkAdapter(relay_url, this.bus, options); + this.network_adapter = new WispNetworkAdapter(relay_url, this.bus, options.net_device); } else { @@ -1389,7 +1389,7 @@ V86.prototype.wait_until_vga_screen_contains = function(text) function put_char(args) { const [row, col, char] = args; - changed_rows.add(col); + changed_rows.add(row); } const check = () => diff --git a/src/browser/wisp_network.js b/src/browser/wisp_network.js index b1346c4fcf..4eb8ee1623 100644 --- a/src/browser/wisp_network.js +++ b/src/browser/wisp_network.js @@ -1,16 +1,14 @@ "use strict"; -const DEFAULT_DOH_SERVER = "cloudflare-dns.com"; - /** * @constructor * + * @param {String} wisp_url * @param {BusConnector} bus * @param {*=} config */ function WispNetworkAdapter(wisp_url, bus, config) { - this.register_ws(wisp_url); this.last_stream = 1; this.connections = {0: {congestion: 0}}; @@ -24,14 +22,15 @@ function WispNetworkAdapter(wisp_url, bus, config) this.vm_ip = new Uint8Array((config.vm_ip || "192.168.86.100").split(".").map(function(x) { return parseInt(x, 10); })); this.masquerade = config.masquerade === undefined || !!config.masquerade; this.vm_mac = new Uint8Array(6); - this.doh_server = config.doh_server || DEFAULT_DOH_SERVER; + this.dns_method = config.dns_method || "doh"; + this.doh_server = config.doh_server; this.tcp_conn = {}; + this.eth_encoder_buf = create_eth_encoder_buf(); this.bus.register("net" + this.id + "-mac", function(mac) { this.vm_mac = new Uint8Array(mac.split(":").map(function(x) { return parseInt(x, 16); })); }, this); - this.bus.register("net" + this.id + "-send", function(data) - { + this.bus.register("net" + this.id + "-send", function(data) { this.send(data); }, this); } @@ -50,14 +49,16 @@ WispNetworkAdapter.prototype.register_ws = function(wisp_url) { }; WispNetworkAdapter.prototype.send_packet = function(data, type, stream_id) { - if(this.connections[stream_id].congestion > 0) { - if(type === "DATA") { - this.connections[stream_id].congestion--; + if(this.connections[stream_id]) { + if(this.connections[stream_id].congestion > 0) { + if(type === "DATA") { + this.connections[stream_id].congestion--; + } + this.wispws.send(data); + } else { + this.connections[stream_id].congested = true; + this.congested_buffer.push({data: data, type: type}); } - this.wispws.send(data); - } else { - this.connections[stream_id].congested = true; - this.congested_buffer.push({data: data, type: type}); } }; /** @@ -167,10 +168,10 @@ WispNetworkAdapter.prototype.send_wisp_frame = function(frame_obj) { const hostname_buffer = new TextEncoder().encode(frame_obj.hostname); full_packet = new Uint8Array(5 + 1 + 2 + hostname_buffer.length); view = new DataView(full_packet.buffer); - view.setUint8(0, 0x01); // TYPE + view.setUint8(0, 0x01); // TYPE view.setUint32(1, frame_obj.stream_id, true); // Stream ID - view.setUint8(5, 0x01); // TCP - view.setUint16(6, frame_obj.port, true); // PORT + view.setUint8(5, 0x01); // TCP + view.setUint16(6, frame_obj.port, true); // PORT full_packet.set(hostname_buffer, 8); // hostname // Setting callbacks @@ -183,16 +184,16 @@ WispNetworkAdapter.prototype.send_wisp_frame = function(frame_obj) { case "DATA": full_packet = new Uint8Array(5 + frame_obj.data.length); view = new DataView(full_packet.buffer); - view.setUint8(0, 0x02); // TYPE + view.setUint8(0, 0x02); // TYPE view.setUint32(1, frame_obj.stream_id, true); // Stream ID full_packet.set(frame_obj.data, 5); // Actual data break; case "CLOSE": full_packet = new Uint8Array(5 + 1); view = new DataView(full_packet.buffer); - view.setUint8(0, 0x04); // TYPE + view.setUint8(0, 0x04); // TYPE view.setUint32(1, frame_obj.stream_id, true); // Stream ID - view.setUint8(5, frame_obj.reason); // close reason + view.setUint8(5, frame_obj.reason); // Packet size break; case "INFO": // No extensions used, turn blank @@ -220,131 +221,63 @@ WispNetworkAdapter.prototype.destroy = function() }; /** - * @param {Uint8Array} data + * @param {Uint8Array} packet + * @param {String} tuple */ -WispNetworkAdapter.prototype.send = function(data) +WispNetworkAdapter.prototype.on_tcp_connection = function(packet, tuple) { - let packet = {}; - parse_eth(data, packet); - - if(packet.tcp) { - let reply = {}; - reply.eth = { ethertype: ETHERTYPE_IPV4, src: this.router_mac, dest: packet.eth.src }; - reply.ipv4 = { - proto: IPV4_PROTO_TCP, - src: packet.ipv4.dest, - dest: packet.ipv4.src - }; - - let tuple = [ - packet.ipv4.src.join("."), - packet.tcp.sport, - packet.ipv4.dest.join("."), - packet.tcp.dport - ].join(":"); - - if(packet.tcp.syn) { - if(this.tcp_conn[tuple]) { - dbg_log("SYN to already opened port", LOG_FETCH); - } - const tcp_conn = new TCPConnection(); - - tcp_conn.state = TCP_STATE_SYN_RECEIVED; - tcp_conn.net = this; - tcp_conn.tuple = tuple; - tcp_conn.stream_id = this.last_stream++; - this.tcp_conn[tuple] = tcp_conn; - - tcp_conn.on_data = (data) => { - if(data.length !== 0) { - this.send_wisp_frame({ - type: "DATA", - stream_id: tcp_conn.stream_id, - data: data - }); - } - }; + let conn = new TCPConnection(); + conn.state = TCP_STATE_SYN_RECEIVED; + conn.net = this; + conn.tuple = tuple; + conn.stream_id = this.last_stream++; + this.tcp_conn[tuple] = conn; + conn.on_data = (data) => { + if(data.length !== 0) { this.send_wisp_frame({ - type: "CONNECT", - stream_id: tcp_conn.stream_id, - hostname: packet.ipv4.dest.join("."), - port: packet.tcp.dport, - data_callback: (data) => { - tcp_conn.write(data); - }, - close_callback: (data) => { - tcp_conn.close(); - } + type: "DATA", + stream_id: conn.stream_id, + data: data }); - - tcp_conn.accept(packet); - return; } + }; - if(!this.tcp_conn[tuple]) { - dbg_log(`I dont know about ${tuple}, so restting`, LOG_FETCH); - let bop = packet.tcp.ackn; - if(packet.tcp.fin || packet.tcp.syn) bop += 1; - reply.tcp = { - sport: packet.tcp.dport, - dport: packet.tcp.sport, - seq: bop, - ackn: packet.tcp.seq + (packet.tcp.syn ? 1: 0), - winsize: packet.tcp.winsize, - rst: true, - ack: packet.tcp.syn - }; - this.receive(make_packet(reply)); - return; - } - - this.tcp_conn[tuple].process(packet); - } - - if(packet.arp && packet.arp.oper === 1 && packet.arp.ptype === ETHERTYPE_IPV4) { - arp_whohas(packet, this); - } - - if(packet.dns) { - // TODO: remove when this wisp client supports udp - (async () => { - let reply = {}; - reply.eth = { ethertype: ETHERTYPE_IPV4, src: this.router_mac, dest: packet.eth.src }; - reply.ipv4 = { - proto: IPV4_PROTO_UDP, - src: this.router_ip, - dest: packet.ipv4.src, - }; - reply.udp = { sport: 53, dport: packet.udp.sport }; - const preferred_fetch = (window.anura?.net?.fetch) || fetch; - const result = await ((await preferred_fetch(`https://${this.doh_server}/dns-query`, {method: "POST", headers: [["content-type", "application/dns-message"]], body: packet.udp.data})).arrayBuffer()); - reply.udp.data = new Uint8Array(result); - this.receive(make_packet(reply)); - })(); - } + conn.on_close = () => { + this.send_wisp_frame({ + type: "CLOSE", + stream_id: conn.stream_id, + reason: 0x02 // 0x02: Voluntary stream closure + }); + }; - if(packet.ntp) { - // TODO: remove when this wisp client supports udp - handle_fake_ntp(packet, this); - return; - } + // WISP doesn't implement shutdown, use close as workaround + conn.on_shutdown = conn.on_close; - // ICMP Ping - if(packet.icmp && packet.icmp.type === 8) { - handle_fake_ping(packet, this); - return; - } + this.send_wisp_frame({ + type: "CONNECT", + stream_id: conn.stream_id, + hostname: packet.ipv4.dest.join("."), + port: packet.tcp.dport, + data_callback: (data) => { + conn.write(data); + }, + close_callback: (data) => { + conn.close(); + } + }); - if(packet.dhcp) { - handle_fake_dhcp(packet, this); - return; - } + conn.accept(packet); + return true; +}; - if(packet.udp && packet.udp.dport === 8) { - // TODO: remove when this wisp client supports udp - handle_udp_echo(packet, this); - } +/** + * @param {Uint8Array} data + */ +WispNetworkAdapter.prototype.send = function(data) +{ + // TODO: forward UDP traffic to WISP server once this WISP client supports UDP + handle_fake_networking(data, this); }; /** diff --git a/src/cpu.js b/src/cpu.js index c1b6210ed0..4eb44d61d0 100644 --- a/src/cpu.js +++ b/src/cpu.js @@ -689,15 +689,15 @@ CPU.prototype.reboot_internal = function() if(this.devices.virtio_9p) { - this.devices.virtio_9p.Reset(); + this.devices.virtio_9p.reset(); } if(this.devices.virtio_console) { - this.devices.virtio_console.Reset(); + this.devices.virtio_console.reset(); } if(this.devices.virtio_net) { - this.devices.virtio_net.Reset(); + this.devices.virtio_net.reset(); } this.load_bios(); @@ -940,7 +940,7 @@ CPU.prototype.init = function(settings, device_bus) this.devices.dma = new DMA(this); - this.devices.vga = new VGAScreen(this, device_bus, settings.screen, settings.vga_memory_size || 8 * 1024 * 1024, settings.screen_options || {}); + this.devices.vga = new VGAScreen(this, device_bus, settings.screen, settings.vga_memory_size || 8 * 1024 * 1024); this.devices.ps2 = new PS2(this, device_bus); @@ -1443,6 +1443,8 @@ CPU.prototype.load_bios = function() return; } + dbg_assert(bios instanceof ArrayBuffer); + // load bios var data = new Uint8Array(bios), start = 0x100000 - bios.byteLength; @@ -1451,6 +1453,8 @@ CPU.prototype.load_bios = function() if(vga_bios) { + dbg_assert(vga_bios instanceof ArrayBuffer); + // load vga bios var vga_bios8 = new Uint8Array(vga_bios); diff --git a/src/lib.js b/src/lib.js index f9cf669cb6..4b322c2e6e 100644 --- a/src/lib.js +++ b/src/lib.js @@ -581,6 +581,7 @@ function load_file(filename, options, n_tries) { if(http.status === 200) { + console.error("Server sent full file in response to ranged request, aborting", { filename }); http.abort(); } }; diff --git a/src/rust/codegen.rs b/src/rust/codegen.rs index aebf46edb1..cba10318ef 100644 --- a/src/rust/codegen.rs +++ b/src/rust/codegen.rs @@ -2648,7 +2648,7 @@ pub fn gen_profiler_stat_increment(builder: &mut WasmBuilder, stat: profiler::st if !cfg!(feature = "profiler") { return; } - let addr = unsafe { profiler::stat_array.as_mut_ptr().offset(stat as isize) } as u32; + let addr = unsafe { &raw mut profiler::stat_array[stat as usize] } as u32; builder.increment_fixed_i64(addr, 1) } diff --git a/src/rust/cpu/cpu.rs b/src/rust/cpu/cpu.rs index 36c4c971b9..ec33eabc9b 100644 --- a/src/rust/cpu/cpu.rs +++ b/src/rust/cpu/cpu.rs @@ -385,6 +385,7 @@ impl SegmentDescriptor { pub fn is_system(&self) -> bool { self.access_byte() & 0x10 == 0 } pub fn system_type(&self) -> u8 { self.access_byte() & 0xF } + pub fn accessed(&self) -> bool { self.access_byte() & 1 == 1 } pub fn is_rw(&self) -> bool { self.access_byte() & 2 == 2 } pub fn is_dc(&self) -> bool { self.access_byte() & 4 == 4 } pub fn is_executable(&self) -> bool { self.access_byte() & 8 == 8 } @@ -407,6 +408,11 @@ impl SegmentDescriptor { raw: self.raw | 2 << 40, } } + pub fn set_accessed(&self) -> SegmentDescriptor { + SegmentDescriptor { + raw: self.raw | 1 << 40, + } + } } pub struct InterruptDescriptor { @@ -586,13 +592,9 @@ pub unsafe fn iret(is_16: bool) { let cs_selector = SegmentSelector::of_u16(new_cs as u16); let cs_descriptor = match return_on_pagefault!(lookup_segment_selector(cs_selector)) { Ok((desc, _)) => desc, - Err(selector_unusable) => match selector_unusable { - SelectorNullOrInvalid::IsNull => { - panic!("Unimplemented: CS selector is null"); - }, - SelectorNullOrInvalid::OutsideOfTableLimit => { - panic!("Unimplemented: CS selector is invalid"); - }, + Err(SelectorNullOrInvalid::IsNull) => panic!("Unimplemented: CS selector is null"), + Err(SelectorNullOrInvalid::OutsideOfTableLimit) => { + panic!("Unimplemented: CS selector is invalid") }, }; @@ -647,18 +649,16 @@ pub unsafe fn iret(is_16: bool) { let ss_selector = SegmentSelector::of_u16(temp_ss as u16); let ss_descriptor = match return_on_pagefault!(lookup_segment_selector(ss_selector)) { Ok((desc, _)) => desc, - Err(selector_unusable) => match selector_unusable { - SelectorNullOrInvalid::IsNull => { - dbg_log!("#GP for loading 0 in SS sel={:x}", temp_ss); - dbg_trace(); - trigger_gp(0); - return; - }, - SelectorNullOrInvalid::OutsideOfTableLimit => { - dbg_log!("#GP for loading invalid in SS sel={:x}", temp_ss); - trigger_gp(temp_ss & !3); - return; - }, + Err(SelectorNullOrInvalid::IsNull) => { + dbg_log!("#GP for loading 0 in SS sel={:x}", temp_ss); + dbg_trace(); + trigger_gp(0); + return; + }, + Err(SelectorNullOrInvalid::OutsideOfTableLimit) => { + dbg_log!("#GP for loading invalid in SS sel={:x}", temp_ss); + trigger_gp(temp_ss & !3); + return; }, }; let new_cpl = cs_selector.rpl(); @@ -852,15 +852,13 @@ pub unsafe fn call_interrupt_vector( SegmentSelector::of_u16(selector as u16) )) { Ok((desc, _)) => desc, - Err(selector_unusable) => match selector_unusable { - SelectorNullOrInvalid::IsNull => { - dbg_log!("is null"); - panic!("Unimplemented: #GP handler"); - }, - SelectorNullOrInvalid::OutsideOfTableLimit => { - dbg_log!("is invalid"); - panic!("Unimplemented: #GP handler (error code)"); - }, + Err(SelectorNullOrInvalid::IsNull) => { + dbg_log!("is null"); + panic!("Unimplemented: #GP handler"); + }, + Err(SelectorNullOrInvalid::OutsideOfTableLimit) => { + dbg_log!("is invalid"); + panic!("Unimplemented: #GP handler (error code)"); }, }; @@ -1113,17 +1111,15 @@ pub unsafe fn far_jump(eip: i32, selector: i32, is_call: bool, is_osize_32: bool let cs_selector = SegmentSelector::of_u16(selector as u16); let info = match return_on_pagefault!(lookup_segment_selector(cs_selector)) { Ok((desc, _)) => desc, - Err(selector_unusable) => match selector_unusable { - SelectorNullOrInvalid::IsNull => { - dbg_log!("#gp null cs"); - trigger_gp(0); - return; - }, - SelectorNullOrInvalid::OutsideOfTableLimit => { - dbg_log!("#gp invalid cs: {:x}", selector); - trigger_gp(selector & !3); - return; - }, + Err(SelectorNullOrInvalid::IsNull) => { + dbg_log!("#gp null cs"); + trigger_gp(0); + return; + }, + Err(SelectorNullOrInvalid::OutsideOfTableLimit) => { + dbg_log!("#gp invalid cs: {:x}", selector); + trigger_gp(selector & !3); + return; }, }; @@ -1154,17 +1150,15 @@ pub unsafe fn far_jump(eip: i32, selector: i32, is_call: bool, is_osize_32: bool SegmentSelector::of_u16(cs_selector as u16) )) { Ok((desc, _)) => desc, - Err(selector_unusable) => match selector_unusable { - SelectorNullOrInvalid::IsNull => { - dbg_log!("#gp null cs"); - trigger_gp(0); - return; - }, - SelectorNullOrInvalid::OutsideOfTableLimit => { - dbg_log!("#gp invalid cs: {:x}", selector); - trigger_gp(selector & !3); - return; - }, + Err(SelectorNullOrInvalid::IsNull) => { + dbg_log!("#gp null cs"); + trigger_gp(0); + return; + }, + Err(SelectorNullOrInvalid::OutsideOfTableLimit) => { + dbg_log!("#gp invalid cs: {:x}", selector); + trigger_gp(selector & !3); + return; }, }; @@ -1198,13 +1192,11 @@ pub unsafe fn far_jump(eip: i32, selector: i32, is_call: bool, is_osize_32: bool let ss_selector = SegmentSelector::of_u16(new_ss as u16); let ss_info = match return_on_pagefault!(lookup_segment_selector(ss_selector)) { Ok((desc, _)) => desc, - Err(selector_unusable) => match selector_unusable { - SelectorNullOrInvalid::IsNull => { - panic!("null ss: {}", new_ss); - }, - SelectorNullOrInvalid::OutsideOfTableLimit => { - panic!("invalid ss: {}", new_ss); - }, + Err(SelectorNullOrInvalid::IsNull) => { + panic!("null ss: {}", new_ss); + }, + Err(SelectorNullOrInvalid::OutsideOfTableLimit) => { + panic!("invalid ss: {}", new_ss); }, }; @@ -1438,17 +1430,15 @@ pub unsafe fn far_return(eip: i32, selector: i32, stack_adjust: i32, is_osize_32 let cs_selector = SegmentSelector::of_u16(selector as u16); let info = match return_on_pagefault!(lookup_segment_selector(cs_selector)) { Ok((desc, _)) => desc, - Err(selector_unusable) => match selector_unusable { - SelectorNullOrInvalid::IsNull => { - dbg_log!("far return: #gp null cs"); - trigger_gp(0); - return; - }, - SelectorNullOrInvalid::OutsideOfTableLimit => { - dbg_log!("far return: #gp invalid cs: {:x}", selector); - trigger_gp(selector & !3); - return; - }, + Err(SelectorNullOrInvalid::IsNull) => { + dbg_log!("far return: #gp null cs"); + trigger_gp(0); + return; + }, + Err(SelectorNullOrInvalid::OutsideOfTableLimit) => { + dbg_log!("far return: #gp invalid cs: {:x}", selector); + trigger_gp(selector & !3); + return; }, }; @@ -2083,7 +2073,15 @@ pub unsafe fn do_page_walk( } let is_in_mapped_range = in_mapped_range(high); - let has_code = !is_in_mapped_range && jit::jit_page_has_code(Page::page_of(high)); + let has_code = if side_effects { + !is_in_mapped_range && jit::jit_page_has_code(Page::page_of(high)) + } + else { + // If side_effects is false, don't call into jit::jit_page_has_code. This value is not used + // anyway (we only get here by translate_address_read_no_side_effects, which only uses the + // address part) + true + }; let info_bits = TLB_VALID | if for_writing { 0 } else { TLB_READONLY } | if allow_user { 0 } else { TLB_NO_USER } @@ -2123,6 +2121,7 @@ pub unsafe fn full_clear_tlb() { valid_tlb_entries_count = 0; if CHECK_TLB_INVARIANTS { + #[allow(static_mut_refs)] for &entry in tlb_data.iter() { dbg_assert!(entry == 0); } @@ -2151,6 +2150,7 @@ pub unsafe fn clear_tlb() { valid_tlb_entries_count = global_page_offset; if CHECK_TLB_INVARIANTS { + #[allow(static_mut_refs)] for &entry in tlb_data.iter() { dbg_assert!(entry == 0 || 0 != entry & TLB_GLOBAL); } @@ -2191,6 +2191,7 @@ pub unsafe fn trigger_gp_jit(code: i32, eip_offset_in_page: i32) { #[no_mangle] pub unsafe fn trigger_fault_end_jit() { + #[allow(static_mut_refs)] let (code, error_code) = jit_fault.take().unwrap(); if DEBUG { if cpu_exception_hook(code) { @@ -2373,16 +2374,23 @@ pub unsafe fn lookup_segment_selector( } let (table_offset, table_limit) = if selector.is_gdt() { - (*gdtr_offset as u32, *gdtr_size as u16) + (*gdtr_offset as u32, *gdtr_size as u32) } else { ( *segment_offsets.offset(LDTR as isize) as u32, - *segment_limits.offset(LDTR as isize) as u16, + *segment_limits.offset(LDTR as isize) as u32, ) }; - if selector.descriptor_offset() > table_limit { + if selector.descriptor_offset() as u32 > table_limit { + dbg_log!( + "segment outside of table limit: selector={:x} offset={:x} isgdt={} table_limit={:x}", + selector.raw, + selector.descriptor_offset(), + selector.is_gdt(), + table_limit + ); return Ok(Err(SelectorNullOrInvalid::OutsideOfTableLimit)); } @@ -2419,41 +2427,34 @@ pub unsafe fn switch_seg(reg: i32, selector_raw: i32) -> bool { } let selector = SegmentSelector::of_u16(selector_raw as u16); - let descriptor = match return_on_pagefault!(lookup_segment_selector(selector), false) { - Ok((desc, _)) => desc, - Err(selector_unusable) => { - // The selector couldn't be used to fetch a descriptor, so we handle all of those - // cases - if selector_unusable == SelectorNullOrInvalid::IsNull { + let (mut descriptor, descriptor_address) = + match return_on_pagefault!(lookup_segment_selector(selector), false) { + Ok(desc) => desc, + Err(SelectorNullOrInvalid::IsNull) => { if reg == SS { dbg_log!("#GP for loading 0 in SS sel={:x}", selector_raw); trigger_gp(0); return false; } - else if reg != CS { + else { // es, ds, fs, gs *sreg.offset(reg as isize) = selector_raw as u16; *segment_is_null.offset(reg as isize) = true; update_state_flags(); return true; } - } - else if selector_unusable == SelectorNullOrInvalid::OutsideOfTableLimit { + }, + Err(SelectorNullOrInvalid::OutsideOfTableLimit) => { dbg_log!( - "#GP for loading invalid in seg={} sel={:x} gdt_limit={:x}", + "#GP for loading invalid in seg={} sel={:x}", reg, selector_raw, - *gdtr_size, ); dbg_trace(); trigger_gp(selector_raw & !3); return false; - } - - dbg_assert!(false); - return false; - }, - }; + }, + }; if reg == SS { if descriptor.is_system() @@ -2474,10 +2475,6 @@ pub unsafe fn switch_seg(reg: i32, selector_raw: i32) -> bool { *stack_size_32 = descriptor.is_32(); } - else if reg == CS { - // handled by switch_cs_real_mode, far_return or far_jump - dbg_assert!(false); - } else { if descriptor.is_system() || !descriptor.is_readable() @@ -2514,6 +2511,15 @@ pub unsafe fn switch_seg(reg: i32, selector_raw: i32) -> bool { } } + if !descriptor.accessed() { + descriptor = descriptor.set_accessed(); + + memory::write8( + translate_address_system_write(descriptor_address + 5).unwrap(), + descriptor.access_byte() as i32, + ); + } + *segment_is_null.offset(reg as isize) = false; *segment_limits.offset(reg as isize) = descriptor.effective_limit(); *segment_offsets.offset(reg as isize) = descriptor.base(); @@ -2573,13 +2579,17 @@ pub unsafe fn load_tr(selector: i32) { *sreg.offset(TR as isize) = selector.raw; // Mark task as busy - safe_write64(descriptor_address, descriptor.set_busy().raw).unwrap(); + memory::write8( + translate_address_system_write(descriptor_address + 5).unwrap(), + descriptor.set_busy().access_byte() as i32, + ); } pub unsafe fn load_ldt(selector: i32) -> OrPageFault<()> { let selector = SegmentSelector::of_u16(selector as u16); if selector.is_null() { + dbg_log!("lldt: null loaded"); *segment_limits.offset(LDTR as isize) = 0; *segment_offsets.offset(LDTR as isize) = 0; *sreg.offset(LDTR as isize) = selector.raw; @@ -2613,6 +2623,12 @@ pub unsafe fn load_ldt(selector: i32) -> OrPageFault<()> { ); } + dbg_log!( + "lldt: {:x} offset={:x} limit={:x}", + selector.raw, + descriptor.base(), + descriptor.effective_limit() + ); *segment_limits.offset(LDTR as isize) = descriptor.effective_limit(); *segment_offsets.offset(LDTR as isize) = descriptor.base(); *sreg.offset(LDTR as isize) = selector.raw; @@ -2919,11 +2935,12 @@ pub unsafe fn cycle_internal() { ); if cfg!(feature = "profiler") { - dbg_assert!(match ::cpu::cpu::debug_last_jump { + dbg_assert!(match debug_last_jump { LastJump::Compiled { .. } => true, _ => false, }); - let last_jump_addr = ::cpu::cpu::debug_last_jump.phys_address().unwrap(); + #[allow(static_mut_refs)] + let last_jump_addr = debug_last_jump.phys_address().unwrap(); let last_jump_opcode = if last_jump_addr != 0 { read32s(last_jump_addr) } @@ -3407,7 +3424,7 @@ pub unsafe fn safe_read_slow_jit( // TODO: Could check if virtual pages point to consecutive physical and go to fast path // do read, write into scratch buffer - let scratch = jit_paging_scratch_buffer.0.as_mut_ptr() as u32; + let scratch = &raw mut jit_paging_scratch_buffer.0 as u32; dbg_assert!(scratch & 0xFFF == 0); for s in addr_low..((addr_low | 0xFFF) + 1) { @@ -3420,7 +3437,7 @@ pub unsafe fn safe_read_slow_jit( ((scratch as i32) ^ addr) & !0xFFF } else if in_mapped_range(addr_low) { - let scratch = jit_paging_scratch_buffer.0.as_mut_ptr(); + let scratch = &raw mut jit_paging_scratch_buffer.0[0]; match bitsize { 128 => ptr::write_unaligned( @@ -3566,7 +3583,7 @@ pub unsafe fn safe_write_slow_jit( }, } - let scratch = jit_paging_scratch_buffer.0.as_mut_ptr() as u32; + let scratch = &raw mut jit_paging_scratch_buffer.0 as u32; dbg_assert!(scratch & 0xFFF == 0); ((scratch as i32) ^ addr) & !0xFFF } @@ -3582,13 +3599,13 @@ pub unsafe fn safe_write_slow_jit( }, } - let scratch = jit_paging_scratch_buffer.0.as_mut_ptr() as u32; + let scratch = &raw mut jit_paging_scratch_buffer.0 as u32; dbg_assert!(scratch & 0xFFF == 0); ((scratch as i32) ^ addr) & !0xFFF } else { if !can_skip_dirty_page { - jit::jit_dirty_page(jit::get_jit_state(), Page::page_of(addr_low)); + jit::jit_dirty_page(Page::page_of(addr_low)); } ((addr_low as i32 + memory::mem8 as i32) ^ addr) & !0xFFF } @@ -3627,7 +3644,7 @@ pub unsafe fn safe_write8(addr: i32, value: i32) -> OrPageFault<()> { } else { if !can_skip_dirty_page { - jit::jit_dirty_page(jit::get_jit_state(), Page::page_of(phys_addr)); + jit::jit_dirty_page(Page::page_of(phys_addr)); } else { dbg_assert!(!jit::jit_page_has_code(Page::page_of(phys_addr as u32))); @@ -3647,7 +3664,7 @@ pub unsafe fn safe_write16(addr: i32, value: i32) -> OrPageFault<()> { } else { if !can_skip_dirty_page { - jit::jit_dirty_page(jit::get_jit_state(), Page::page_of(phys_addr)); + jit::jit_dirty_page(Page::page_of(phys_addr)); } else { dbg_assert!(!jit::jit_page_has_code(Page::page_of(phys_addr as u32))); @@ -3671,7 +3688,7 @@ pub unsafe fn safe_write32(addr: i32, value: i32) -> OrPageFault<()> { } else { if !can_skip_dirty_page { - jit::jit_dirty_page(jit::get_jit_state(), Page::page_of(phys_addr)); + jit::jit_dirty_page(Page::page_of(phys_addr)); } else { dbg_assert!(!jit::jit_page_has_code(Page::page_of(phys_addr as u32))); @@ -3694,7 +3711,7 @@ pub unsafe fn safe_write64(addr: i32, value: u64) -> OrPageFault<()> { } else { if !can_skip_dirty_page { - jit::jit_dirty_page(jit::get_jit_state(), Page::page_of(phys_addr)); + jit::jit_dirty_page(Page::page_of(phys_addr)); } else { dbg_assert!(!jit::jit_page_has_code(Page::page_of(phys_addr as u32))); @@ -3718,7 +3735,7 @@ pub unsafe fn safe_write128(addr: i32, value: reg128) -> OrPageFault<()> { } else { if !can_skip_dirty_page { - jit::jit_dirty_page(jit::get_jit_state(), Page::page_of(phys_addr)); + jit::jit_dirty_page(Page::page_of(phys_addr)); } else { dbg_assert!(!jit::jit_page_has_code(Page::page_of(phys_addr as u32))); @@ -3740,10 +3757,10 @@ pub unsafe fn safe_read_write8(addr: i32, instruction: &dyn Fn(i32) -> i32) { } else { if !can_skip_dirty_page { - ::jit::jit_dirty_page(::jit::get_jit_state(), Page::page_of(phys_addr)); + jit::jit_dirty_page(Page::page_of(phys_addr)); } else { - dbg_assert!(!::jit::jit_page_has_code(Page::page_of(phys_addr as u32))); + dbg_assert!(!jit::jit_page_has_code(Page::page_of(phys_addr as u32))); } memory::write8_no_mmap_or_dirty_check(phys_addr, value); } @@ -3766,10 +3783,10 @@ pub unsafe fn safe_read_write16(addr: i32, instruction: &dyn Fn(i32) -> i32) { } else { if !can_skip_dirty_page { - ::jit::jit_dirty_page(::jit::get_jit_state(), Page::page_of(phys_addr)); + jit::jit_dirty_page(Page::page_of(phys_addr)); } else { - dbg_assert!(!::jit::jit_page_has_code(Page::page_of(phys_addr as u32))); + dbg_assert!(!jit::jit_page_has_code(Page::page_of(phys_addr as u32))); } memory::write16_no_mmap_or_dirty_check(phys_addr, value); }; @@ -3794,10 +3811,10 @@ pub unsafe fn safe_read_write32(addr: i32, instruction: &dyn Fn(i32) -> i32) { } else { if !can_skip_dirty_page { - ::jit::jit_dirty_page(::jit::get_jit_state(), Page::page_of(phys_addr)); + jit::jit_dirty_page(Page::page_of(phys_addr)); } else { - dbg_assert!(!::jit::jit_page_has_code(Page::page_of(phys_addr as u32))); + dbg_assert!(!jit::jit_page_has_code(Page::page_of(phys_addr as u32))); } memory::write32_no_mmap_or_dirty_check(phys_addr, value); }; @@ -4059,8 +4076,8 @@ pub unsafe fn read_tsc() -> u64 { if TSC_VERBOSE_LOGGING || tsc_last_extra >= tsc_resolution { dbg_log!( "rdtsc: jump from {}+{} to {} (diff {}, {}%)", - tsc_last_value, - tsc_last_extra, + tsc_last_value as u64, + tsc_last_extra as u64, value, value - (tsc_last_value + tsc_last_extra), (100 * tsc_last_extra) / tsc_resolution, @@ -4243,9 +4260,11 @@ pub unsafe fn trigger_ss(code: i32) { pub unsafe fn store_current_tsc() { *current_tsc = read_tsc(); } #[no_mangle] -pub unsafe fn handle_irqs() { +pub unsafe fn handle_irqs() { handle_irqs_internal(&mut pic::get_pic()) } + +pub unsafe fn handle_irqs_internal(pic: &mut pic::Pic) { if *flags & FLAG_INTERRUPT != 0 { - if let Some(irq) = pic::pic_acknowledge_irq() { + if let Some(irq) = pic::pic_acknowledge_irq(pic) { pic_call_irq(irq) } else if *acpi_enabled { @@ -4370,7 +4389,7 @@ pub unsafe fn reset_cpu() { update_state_flags(); - jit::jit_clear_cache(jit::get_jit_state()); + jit::jit_clear_cache_js(); } #[no_mangle] diff --git a/src/rust/cpu/memory.rs b/src/rust/cpu/memory.rs index 76c343b3b3..9141c4ebb5 100644 --- a/src/rust/cpu/memory.rs +++ b/src/rust/cpu/memory.rs @@ -15,6 +15,7 @@ mod ext { use cpu::cpu::reg128; use cpu::global_pointers::memory_size; use cpu::vga; +use jit; use page::Page; use std::alloc; @@ -51,17 +52,17 @@ pub fn svga_allocate_memory(size: u32) -> u32 { dbg_assert!(vga_mem8.is_null()); }; let layout = alloc::Layout::from_size_align(size as usize, 0x1000).unwrap(); - let ptr = unsafe { alloc::alloc(layout) as u32 }; + let ptr = unsafe { alloc::alloc(layout) }; dbg_assert!( size & (1 << 12 << 6) == 0, "size not aligned to dirty_bitmap" ); unsafe { - vga_mem8 = ptr as *mut u8; + vga_mem8 = ptr; vga_memory_size = size; - vga::dirty_bitmap.resize((size >> 12 >> 6) as usize, 0); + vga::set_dirty_bitmap_size(size >> 12 >> 6); }; - ptr + ptr as u32 } #[no_mangle] @@ -172,7 +173,7 @@ pub unsafe fn write8(addr: u32, value: i32) { mmap_write8(addr, value & 0xFF); } else { - ::jit::jit_dirty_page(::jit::get_jit_state(), Page::page_of(addr)); + jit::jit_dirty_page(Page::page_of(addr)); write8_no_mmap_or_dirty_check(addr, value); }; } @@ -187,7 +188,7 @@ pub unsafe fn write16(addr: u32, value: i32) { mmap_write16(addr, value & 0xFFFF); } else { - ::jit::jit_dirty_cache_small(addr, addr + 2); + jit::jit_dirty_cache_small(addr, addr + 2); write16_no_mmap_or_dirty_check(addr, value); }; } @@ -201,7 +202,7 @@ pub unsafe fn write32(addr: u32, value: i32) { mmap_write32(addr, value); } else { - ::jit::jit_dirty_cache_small(addr, addr + 4); + jit::jit_dirty_cache_small(addr, addr + 4); write32_no_mmap_or_dirty_check(addr, value); }; } diff --git a/src/rust/cpu/pic.rs b/src/rust/cpu/pic.rs index 613854dceb..44fd6f8209 100644 --- a/src/rust/cpu/pic.rs +++ b/src/rust/cpu/pic.rs @@ -5,12 +5,14 @@ pub const PIC_LOG: bool = false; pub const PIC_LOG_VERBOSE: bool = false; + use cpu::cpu; +use std::sync::{Mutex, MutexGuard}; // Note: This layout is deliberately chosen to match the old JavaScript pic state // (cpu.get_state_pic depens on this layout) #[repr(C, packed)] -struct Pic { +struct Pic0 { irq_mask: u8, irq_map: u8, @@ -37,53 +39,59 @@ struct Pic { special_mask_mode: bool, } -#[allow(non_upper_case_globals)] -static mut master: Pic = Pic { - // all irqs off - irq_mask: 0, - // Bogus default value (both master and slave mapped to 0). - // Will be initialized by the BIOS - irq_map: 0, - // in-service register - // Holds interrupts that are currently being serviced - isr: 0, - // interrupt request register - // Holds interrupts that have been requested - irr: 0, - irq_value: 0, - expect_icw4: false, - state: 0, - read_isr: false, - auto_eoi: false, - special_mask_mode: false, - elcr: 0, - master: true, - dummy: 0, -}; - -#[allow(non_upper_case_globals)] -static mut slave: Pic = Pic { - // all irqs off - irq_mask: 0, - // Bogus default value (both master and slave mapped to 0). - // Will be initialized by the BIOS - irq_map: 0, - // in-service register - // Holds interrupts that are currently being serviced - isr: 0, - // interrupt request register - // Holds interrupts that have been requested - irr: 0, - irq_value: 0, - expect_icw4: false, - state: 0, - read_isr: false, - auto_eoi: false, - special_mask_mode: false, - elcr: 0, - master: false, - dummy: 0, -}; +pub struct Pic { + master: Pic0, + slave: Pic0, +} + +static PIC: Mutex = Mutex::new(Pic { + master: Pic0 { + // all irqs off + irq_mask: 0, + // Bogus default value (both master and slave mapped to 0). + // Will be initialized by the BIOS + irq_map: 0, + // in-service register + // Holds interrupts that are currently being serviced + isr: 0, + // interrupt request register + // Holds interrupts that have been requested + irr: 0, + irq_value: 0, + expect_icw4: false, + state: 0, + read_isr: false, + auto_eoi: false, + special_mask_mode: false, + elcr: 0, + master: true, + dummy: 0, + }, + slave: Pic0 { + // all irqs off + irq_mask: 0, + // Bogus default value (both master and slave mapped to 0). + // Will be initialized by the BIOS + irq_map: 0, + // in-service register + // Holds interrupts that are currently being serviced + isr: 0, + // interrupt request register + // Holds interrupts that have been requested + irr: 0, + irq_value: 0, + expect_icw4: false, + state: 0, + read_isr: false, + auto_eoi: false, + special_mask_mode: false, + elcr: 0, + master: false, + dummy: 0, + }, +}); + +pub fn get_pic() -> MutexGuard<'static, Pic> { PIC.try_lock().unwrap() } // Checking for callable interrupts: // (cpu changes interrupt flag) -> cpu.handle_irqs -> pic_acknowledge_irq @@ -92,7 +100,7 @@ static mut slave: Pic = Pic { // triggering irqs: // (io device has irq) -> cpu.device_raise_irq -> pic.set_irq -> pic.check_irqs -> cpu.handle_irqs -> (see above) -impl Pic { +impl Pic0 { unsafe fn get_irq(&mut self) -> Option { let enabled_irr = self.irr & self.irq_mask; @@ -136,78 +144,73 @@ impl Pic { Some(irq_number) } - unsafe fn check_irqs(&mut self) { - let is_set = self.get_irq().is_some(); + unsafe fn port0_read(self: &Pic0) -> u32 { + (if self.read_isr { self.isr } else { self.irr }) as u32 + } + unsafe fn port1_read(self: &Pic0) -> u32 { !self.irq_mask as u32 } +} - if self.master { - if is_set { - cpu::handle_irqs(); - } - } - else { - if is_set { - master.set_irq(2) +impl Pic { + unsafe fn set_irq(self: &mut Pic, i: u8) { + let mask = 1 << (i & 7); + let dev = if i < 8 { &mut self.master } else { &mut self.slave }; + if dev.irq_value & mask == 0 || dev.elcr & mask != 0 { + dev.irr |= mask; + dev.irq_value |= mask; + if i < 8 { + self.check_irqs_master() } else { - master.clear_irq(2) + self.check_irqs_slave() } } } - unsafe fn set_irq(&mut self, i: u8) { - let mask = 1 << i; - if self.irq_value & mask == 0 || self.elcr & mask != 0 { - self.irr |= mask; - self.irq_value |= mask; - self.check_irqs() - } - } - - unsafe fn clear_irq(&mut self, i: u8) { - let mask = 1 << i; - if self.elcr & mask != 0 { - self.irq_value &= !mask; - self.irr &= !mask; - self.check_irqs() - } - else if self.irq_value & mask != 0 { - self.irq_value &= !mask; - self.check_irqs() + unsafe fn clear_irq(self: &mut Pic, i: u8) { + let mask = 1 << (i & 7); + let dev = if i < 8 { &mut self.master } else { &mut self.slave }; + dev.irq_value &= !mask; + if dev.elcr & mask != 0 { + dev.irr &= !mask; + if i < 8 { + self.check_irqs_master() + } + else { + self.check_irqs_slave() + } } } - unsafe fn port0_read(self: &Pic) -> u32 { - (if self.read_isr { self.isr } else { self.irr }) as u32 - } - unsafe fn port1_read(self: &Pic) -> u32 { !self.irq_mask as u32 } - - unsafe fn port0_write(&mut self, v: u8) { + unsafe fn port0_write(&mut self, index: u8, v: u8) { + let dev = if index == 0 { &mut self.master } else { &mut self.slave }; if v & 0x10 != 0 { // xxxx1xxx // icw1 dbg_log!("icw1 = {:x}", v); - self.isr = 0; - self.irr = 0; - self.irq_mask = 0xff; - self.irq_value = 0; - self.auto_eoi = true; - - self.expect_icw4 = v & 1 != 0; - self.state = 1; + dev.isr = 0; + dev.irr = 0; + dev.irq_mask = 0xff; + dev.irq_value = 0; + dev.auto_eoi = true; + + dev.expect_icw4 = v & 1 != 0; + dbg_assert!(v & 2 == 0, "unimplemented: single mode"); + dbg_assert!(v & 8 == 0, "unimplemented: level mode"); + dev.state = 1; } else if v & 8 != 0 { // xxx01xxx // ocw3 dbg_log!("ocw3: {:x}", v); if v & 2 != 0 { - self.read_isr = v & 1 != 0; + dev.read_isr = v & 1 != 0; } if v & 4 != 0 { dbg_assert!(false, "unimplemented: polling"); } if v & 0x40 != 0 { - self.special_mask_mode = (v & 0x20) == 0x20; - dbg_log!("special mask mode: {}", self.special_mask_mode); + dev.special_mask_mode = (v & 0x20) == 0x20; + dbg_log!("special mask mode: {}", dev.special_mask_mode); } } else { @@ -222,76 +225,100 @@ impl Pic { if eoi_type == 1 { // non-specific eoi - self.isr &= self.isr - 1; + dev.isr &= dev.isr - 1; if PIC_LOG { - dbg_log!("new isr: {:x}", self.isr); + dbg_log!("new isr: {:x}", dev.isr); } } else if eoi_type == 3 { // specific eoi - self.isr &= !(1 << (v & 7)); + dev.isr &= !(1 << (v & 7)); } - else if (v & 0xC8) == 0xC0 { - // os2 v4 + else if eoi_type == 6 { + // os2 v4, freebsd let priority = v & 7; dbg_log!("lowest priority: {:x}", priority); } else { - dbg_log!("Unknown eoi: {:x}", v); + dbg_log!("Unknown eoi: {:x} type={:x}", v, eoi_type); dbg_assert!(false); - self.isr &= self.isr - 1; + dev.isr &= dev.isr - 1; } - self.check_irqs() + if index == 0 { + self.check_irqs_master() + } + else { + self.check_irqs_slave() + } } } - unsafe fn port1_write(&mut self, v: u8) { - //dbg_log!("21 write: " + h(v)); - if self.state == 0 { - if self.expect_icw4 { + unsafe fn port1_write(&mut self, index: u8, v: u8) { + let dev = if index == 0 { &mut self.master } else { &mut self.slave }; + if dev.state == 0 { + if dev.expect_icw4 { // icw4 - self.expect_icw4 = false; - self.auto_eoi = v & 2 != 0; - dbg_log!("icw4: {:x} autoeoi={}", v, self.auto_eoi); - - if v & 1 == 0 { - dbg_assert!(false, "unimplemented: not 8086 mode"); - } + dev.expect_icw4 = false; + dev.auto_eoi = v & 2 != 0; + dbg_log!("icw4: {:x} autoeoi={}", v, dev.auto_eoi); + dbg_assert!(v & 0x10 == 0, "unimplemented: nested mode"); + dbg_assert!(v & 1 == 1, "unimplemented: 8086/88 mode"); } else { // ocw1 - self.irq_mask = !v; + dev.irq_mask = !v; if PIC_LOG_VERBOSE { - dbg_log!("interrupt mask: {:x}", self.irq_mask); + dbg_log!("interrupt mask: {:x}", dev.irq_mask); } - self.check_irqs() + if index == 0 { + self.check_irqs_master() + } + else { + self.check_irqs_slave() + } } } - else if self.state == 1 { + else if dev.state == 1 { // icw2 - self.irq_map = v; - dbg_log!("interrupts are mapped to {:x}", self.irq_map); - self.state += 1; + dev.irq_map = v; + dbg_log!("interrupts are mapped to {:x}", dev.irq_map); + dev.state += 1; } - else if self.state == 2 { + else if dev.state == 2 { // icw3 - self.state = 0; + dev.state = 0; dbg_log!("icw3: {:x}", v); } } + + unsafe fn check_irqs_master(&mut self) { + let is_set = self.master.get_irq().is_some(); + if is_set { + cpu::handle_irqs_internal(self); + } + } + unsafe fn check_irqs_slave(&mut self) { + let is_set = self.slave.get_irq().is_some(); + if is_set { + self.set_irq(2) + } + else { + self.clear_irq(2) + } + } } // called by the cpu -pub unsafe fn pic_acknowledge_irq() -> Option { - let irq = match master.get_irq() { +pub unsafe fn pic_acknowledge_irq(pic: &mut Pic) -> Option { + let irq = match pic.master.get_irq() { Some(i) => i, None => return None, }; - if master.irr == 0 { + if pic.master.irr == 0 { dbg_assert!(false); //PIC_LOG_VERBOSE && dbg_log!("master> spurious requested=" + irq); //Some(pic.irq_map | 7) @@ -300,36 +327,36 @@ pub unsafe fn pic_acknowledge_irq() -> Option { let mask = 1 << irq; - if master.elcr & mask == 0 { + if pic.master.elcr & mask == 0 { // not in level mode - master.irr &= !mask; + pic.master.irr &= !mask; } - if !master.auto_eoi { - master.isr |= mask; + if !pic.master.auto_eoi { + pic.master.isr |= mask; } if PIC_LOG_VERBOSE { dbg_log!("[PIC] master> acknowledge {}", irq); } - master.check_irqs(); + //pic.check_irqs_master(); // XXX if irq == 2 { - acknowledge_irq_slave() + acknowledge_irq_slave(pic) } else { - Some(master.irq_map | irq) + Some(pic.master.irq_map | irq) } } -unsafe fn acknowledge_irq_slave() -> Option { - let irq = match slave.get_irq() { +unsafe fn acknowledge_irq_slave(pic: &mut Pic) -> Option { + let irq = match pic.slave.get_irq() { Some(i) => i, None => return None, }; - if slave.irr == 0 { + if pic.slave.irr == 0 { //PIC_LOG_VERBOSE && dbg_log!("slave> spurious requested=" + irq); //Some(pic.irq_map | 7) dbg_assert!(false); @@ -338,21 +365,21 @@ unsafe fn acknowledge_irq_slave() -> Option { let mask = 1 << irq; - if slave.elcr & mask == 0 { + if pic.slave.elcr & mask == 0 { // not in level mode - slave.irr &= !mask; + pic.slave.irr &= !mask; } - if !slave.auto_eoi { - slave.isr |= mask; + if !pic.slave.auto_eoi { + pic.slave.isr |= mask; } if PIC_LOG_VERBOSE { dbg_log!("[PIC] slave> acknowledge {}", irq); } - slave.check_irqs(); + //pic.check_irqs_slave(); // XXX - Some(slave.irq_map | irq) + Some(pic.slave.irq_map | irq) } // called by javascript @@ -361,15 +388,10 @@ pub unsafe fn pic_set_irq(i: u8) { dbg_assert!(i < 16); if PIC_LOG_VERBOSE { - dbg_log!("[PIC] set irq {}, irq_value={:x}", i, master.irq_value); + dbg_log!("[PIC] set irq {}", i); } - if i < 8 { - master.set_irq(i) - } - else { - slave.set_irq(i - 8) - } + get_pic().set_irq(i) } // called by javascript @@ -381,44 +403,39 @@ pub unsafe fn pic_clear_irq(i: u8) { dbg_log!("[PIC] clear irq {}", i); } - if i < 8 { - master.clear_irq(i) - } - else { - slave.clear_irq(i - 8) - } + get_pic().clear_irq(i) } #[no_mangle] -pub unsafe fn port20_read() -> u32 { master.port0_read() } +pub unsafe fn port20_read() -> u32 { get_pic().master.port0_read() } #[no_mangle] -pub unsafe fn port21_read() -> u32 { master.port1_read() } +pub unsafe fn port21_read() -> u32 { get_pic().master.port1_read() } #[no_mangle] -pub unsafe fn portA0_read() -> u32 { slave.port0_read() } +pub unsafe fn portA0_read() -> u32 { get_pic().slave.port0_read() } #[no_mangle] -pub unsafe fn portA1_read() -> u32 { slave.port1_read() } +pub unsafe fn portA1_read() -> u32 { get_pic().slave.port1_read() } #[no_mangle] -pub unsafe fn port20_write(v: u8) { master.port0_write(v) } +pub unsafe fn port20_write(v: u8) { get_pic().port0_write(0, v) } #[no_mangle] -pub unsafe fn port21_write(v: u8) { master.port1_write(v) } +pub unsafe fn port21_write(v: u8) { get_pic().port1_write(0, v) } #[no_mangle] -pub unsafe fn portA0_write(v: u8) { slave.port0_write(v) } +pub unsafe fn portA0_write(v: u8) { get_pic().port0_write(1, v) } #[no_mangle] -pub unsafe fn portA1_write(v: u8) { slave.port1_write(v) } +pub unsafe fn portA1_write(v: u8) { get_pic().port1_write(1, v) } #[no_mangle] -pub unsafe fn port4D0_read() -> u32 { master.elcr as u32 } +pub unsafe fn port4D0_read() -> u32 { get_pic().master.elcr as u32 } #[no_mangle] -pub unsafe fn port4D1_read() -> u32 { slave.elcr as u32 } +pub unsafe fn port4D1_read() -> u32 { get_pic().slave.elcr as u32 } #[no_mangle] -pub unsafe fn port4D0_write(v: u8) { master.elcr = v } +pub unsafe fn port4D0_write(v: u8) { get_pic().master.elcr = v } #[no_mangle] -pub unsafe fn port4D1_write(v: u8) { slave.elcr = v } +pub unsafe fn port4D1_write(v: u8) { get_pic().slave.elcr = v } #[no_mangle] -pub unsafe fn get_pic_addr_master() -> u32 { std::ptr::addr_of_mut!(master) as u32 } +pub unsafe fn get_pic_addr_master() -> u32 { &raw const get_pic().master as u32 } #[no_mangle] -pub unsafe fn get_pic_addr_slave() -> u32 { std::ptr::addr_of_mut!(slave) as u32 } +pub unsafe fn get_pic_addr_slave() -> u32 { &raw const get_pic().slave as u32 } diff --git a/src/rust/cpu/string.rs b/src/rust/cpu/string.rs index 0f1500a127..ba639a6c76 100644 --- a/src/rust/cpu/string.rs +++ b/src/rust/cpu/string.rs @@ -23,6 +23,7 @@ use cpu::memory::{ memset_no_mmap_or_dirty_check, read16_no_mmap_check, read32_no_mmap_check, read8_no_mmap_check, write16_no_mmap_or_dirty_check, write32_no_mmap_or_dirty_check, write8_no_mmap_or_dirty_check, }; +use jit; use page::Page; fn count_until_end_of_page(direction: i32, size: i32, addr: u32) -> u32 { @@ -248,7 +249,7 @@ unsafe fn string_instruction( dbg_assert!(count_until_end_of_page > 0); if !skip_dirty_page { - ::jit::jit_dirty_page(::jit::get_jit_state(), Page::page_of(phys_dst)); + jit::jit_dirty_page(Page::page_of(phys_dst)); } let mut rep_cmp_finished = false; diff --git a/src/rust/cpu/vga.rs b/src/rust/cpu/vga.rs index 84c722cbcd..976abca0c6 100644 --- a/src/rust/cpu/vga.rs +++ b/src/rust/cpu/vga.rs @@ -1,20 +1,24 @@ #![allow(non_upper_case_globals)] +#![allow(static_mut_refs)] + +// Safety of allow(static_mut_refs) in this file: +// These following two globals are not passed anywhere, only built-in function are called on them +static mut dirty_bitmap: Vec = Vec::new(); +static mut dest_buffer: Vec = Vec::new(); use cpu::global_pointers; use cpu::memory; use std::ptr; -pub static mut dirty_bitmap: Vec = Vec::new(); -pub static mut dest_buffer: Vec = Vec::new(); - #[no_mangle] pub unsafe fn svga_allocate_dest_buffer(size: u32) -> u32 { dest_buffer.resize(size as usize, 0); dest_buffer.as_mut_ptr() as u32 } -#[no_mangle] +pub unsafe fn set_dirty_bitmap_size(size: u32) { dirty_bitmap.resize(size as usize, 0); } + pub unsafe fn mark_dirty(addr: u32) { let page = (addr - memory::VGA_LFB_ADDRESS) >> 12; dbg_assert!(((page >> 6) as usize) < dirty_bitmap.len()); diff --git a/src/rust/jit.rs b/src/rust/jit.rs index b1d782b331..0aabee5e92 100644 --- a/src/rust/jit.rs +++ b/src/rust/jit.rs @@ -1,7 +1,9 @@ use std::collections::{BTreeMap, HashMap, HashSet, VecDeque}; use std::iter::FromIterator; -use std::mem; +use std::mem::{self, MaybeUninit}; +use std::ops::{Deref, DerefMut}; use std::ptr::NonNull; +use std::sync::{Mutex, MutexGuard}; use analysis::AnalysisType; use codegen; @@ -82,18 +84,27 @@ pub const CHECK_JIT_STATE_INVARIANTS: bool = false; const MAX_INSTRUCTION_LENGTH: u32 = 16; -#[allow(non_upper_case_globals)] -static mut jit_state: NonNull = - unsafe { NonNull::new_unchecked(mem::align_of::() as *mut _) }; +static JIT_STATE: Mutex> = Mutex::new(MaybeUninit::uninit()); +fn get_jit_state() -> JitStateRef { JitStateRef(JIT_STATE.try_lock().unwrap()) } -pub fn get_jit_state() -> &'static mut JitState { unsafe { jit_state.as_mut() } } +struct JitStateRef(MutexGuard<'static, MaybeUninit>); + +impl Deref for JitStateRef { + type Target = JitState; + fn deref(&self) -> &Self::Target { unsafe { self.0.assume_init_ref() } } +} +impl DerefMut for JitStateRef { + fn deref_mut(&mut self) -> &mut Self::Target { unsafe { self.0.assume_init_mut() } } +} #[no_mangle] pub fn rust_init() { dbg_assert!(std::mem::size_of::<[Option>; 0x100000]>() == 0x100000 * 4); - let x = Box::new(JitState::create_and_initialise()); - unsafe { jit_state = NonNull::new(Box::into_raw(x)).unwrap() } + let _ = JIT_STATE + .try_lock() + .unwrap() + .write(JitState::create_and_initialise()); use std::panic; @@ -114,7 +125,7 @@ enum CompilingPageState { CompilingWritten, } -pub struct JitState { +struct JitState { wasm_builder: WasmBuilder, // as an alternative to HashSet, we could use a bitmap of 4096 bits here @@ -127,7 +138,7 @@ pub struct JitState { compiling: Option<(WasmTableIndex, CompilingPageState)>, } -pub fn check_jit_state_invariants(ctx: &mut JitState) { +fn check_jit_state_invariants(ctx: &mut JitState) { if !CHECK_JIT_STATE_INVARIANTS { return; } @@ -1011,7 +1022,7 @@ pub fn codegen_finalize_finished( phys_addr: u32, state_flags: CachedStateFlags, ) { - let ctx = get_jit_state(); + let mut ctx = get_jit_state(); dbg_assert!(wasm_table_index != WasmTableIndex(0)); @@ -1029,8 +1040,8 @@ pub fn codegen_finalize_finished( dbg_assert!(wasm_table_index == in_progress_wasm_table_index); profiler::stat_increment(stat::INVALIDATE_MODULE_WRITTEN_WHILE_COMPILED); - free_wasm_table_index(ctx, wasm_table_index); - check_jit_state_invariants(ctx); + free_wasm_table_index(&mut ctx, wasm_table_index); + check_jit_state_invariants(&mut ctx); return; }, Some((in_progress_wasm_table_index, CompilingPageState::Compiling { pages })) => { @@ -1083,10 +1094,10 @@ pub fn codegen_finalize_finished( dbg_log!("unused after overwrite {}", index.to_u16()); profiler::stat_increment(stat::INVALIDATE_MODULE_UNUSED_AFTER_OVERWRITE); - free_wasm_table_index(ctx, index); + free_wasm_table_index(&mut ctx, index); } - check_jit_state_invariants(ctx); + check_jit_state_invariants(&mut ctx); } pub fn update_tlb_code(virt_page: Page, phys_page: Page) { @@ -2090,7 +2101,8 @@ pub fn jit_increase_hotness_and_maybe_compile( return; } - let ctx = get_jit_state(); + let mut ctx = get_jit_state(); + let is_compiling = ctx.compiling.is_some(); let page = Page::page_of(phys_address); let (hotness, entry_points) = ctx.entry_points.entry(page).or_insert_with(|| { cpu::tlb_set_has_code(page, true); @@ -2104,18 +2116,18 @@ pub fn jit_increase_hotness_and_maybe_compile( *hotness += heat; if *hotness >= JIT_THRESHOLD { - if ctx.compiling.is_some() { + if is_compiling { return; } // only try generating if we're in the correct address space if cpu::translate_address_read_no_side_effects(virt_address) == Ok(phys_address) { *hotness = 0; - jit_analyze_and_generate(ctx, virt_address, phys_address, cs_offset, state_flags) + jit_analyze_and_generate(&mut ctx, virt_address, phys_address, cs_offset, state_flags) } else { profiler::stat_increment(stat::COMPILE_WRONG_ADDRESS_SPACE); } - }; + } } fn free_wasm_table_index(ctx: &mut JitState, wasm_table_index: WasmTableIndex) { @@ -2164,7 +2176,7 @@ fn free_wasm_table_index(ctx: &mut JitState, wasm_table_index: WasmTableIndex) { } /// Register a write in this page: Delete all present code -pub fn jit_dirty_page(ctx: &mut JitState, page: Page) { +fn jit_dirty_page_ctx(ctx: &mut JitState, page: Page) { let mut did_have_code = false; if let Some(PageInfo { @@ -2269,10 +2281,13 @@ pub fn jit_dirty_cache(start_addr: u32, end_addr: u32) { let end_page = Page::page_of(end_addr - 1); for page in start_page.to_u32()..end_page.to_u32() + 1 { - jit_dirty_page(get_jit_state(), Page::page_of(page << 12)); + jit_dirty_page_ctx(&mut get_jit_state(), Page::page_of(page << 12)); } } +#[no_mangle] +pub fn jit_dirty_page(page: Page) { jit_dirty_page_ctx(&mut get_jit_state(), page) } + /// dirty pages in the range of start_addr and end_addr, which must span at most two pages pub fn jit_dirty_cache_small(start_addr: u32, end_addr: u32) { dbg_assert!(start_addr < end_addr); @@ -2280,21 +2295,21 @@ pub fn jit_dirty_cache_small(start_addr: u32, end_addr: u32) { let start_page = Page::page_of(start_addr); let end_page = Page::page_of(end_addr - 1); - let ctx = get_jit_state(); - jit_dirty_page(ctx, start_page); + let mut ctx = get_jit_state(); + jit_dirty_page_ctx(&mut ctx, start_page); // Note: This can't happen when paging is enabled, as writes across // boundaries are split up on two pages if start_page != end_page { dbg_assert!(start_page.to_u32() + 1 == end_page.to_u32()); - jit_dirty_page(ctx, end_page); + jit_dirty_page_ctx(&mut ctx, end_page); } } #[no_mangle] -pub fn jit_clear_cache_js() { jit_clear_cache(get_jit_state()) } +pub fn jit_clear_cache_js() { jit_clear_cache(&mut get_jit_state()) } -pub fn jit_clear_cache(ctx: &mut JitState) { +fn jit_clear_cache(ctx: &mut JitState) { let mut pages_with_code = HashSet::new(); for &p in ctx.entry_points.keys() { @@ -2305,13 +2320,13 @@ pub fn jit_clear_cache(ctx: &mut JitState) { } for page in pages_with_code { - jit_dirty_page(ctx, page); + jit_dirty_page_ctx(ctx, page); } } -pub fn jit_page_has_code(page: Page) -> bool { jit_page_has_code_ctx(get_jit_state(), page) } +pub fn jit_page_has_code(page: Page) -> bool { jit_page_has_code_ctx(&mut get_jit_state(), page) } -pub fn jit_page_has_code_ctx(ctx: &mut JitState, page: Page) -> bool { +fn jit_page_has_code_ctx(ctx: &mut JitState, page: Page) -> bool { ctx.pages.contains_key(&page) || ctx.entry_points.contains_key(&page) } @@ -2347,7 +2362,9 @@ pub fn check_missed_entry_points(phys_address: u32, state_flags: CachedStateFlag return; } + #[allow(static_mut_refs)] let last_jump_type = unsafe { cpu::debug_last_jump.name() }; + #[allow(static_mut_refs)] let last_jump_addr = unsafe { cpu::debug_last_jump.phys_address() }.unwrap_or(0); let last_jump_opcode = if last_jump_addr != 0 { memory::read32s(last_jump_addr) } else { 0 }; diff --git a/src/rust/profiler.rs b/src/rust/profiler.rs index 50ed4211fd..4c8d0f4b43 100644 --- a/src/rust/profiler.rs +++ b/src/rust/profiler.rs @@ -134,6 +134,7 @@ pub fn stat_increment_by(stat: stat, by: u64) { #[no_mangle] pub fn profiler_init() { unsafe { + #[allow(static_mut_refs)] for x in stat_array.iter_mut() { *x = 0 } diff --git a/src/vga.js b/src/vga.js index 2f34ccb738..0d85da06a7 100644 --- a/src/vga.js +++ b/src/vga.js @@ -50,9 +50,8 @@ const VGA_HOST_MEMORY_SPACE_SIZE = Uint32Array.from([ * @param {BusConnector} bus * @param {ScreenAdapter|DummyScreenAdapter} screen * @param {number} vga_memory_size - * @param {Object} options */ -function VGAScreen(cpu, bus, screen, vga_memory_size, options) +function VGAScreen(cpu, bus, screen, vga_memory_size) { this.cpu = cpu; @@ -294,6 +293,7 @@ function VGAScreen(cpu, bus, screen, vga_memory_size, options) this.miscellaneous_output_register = 0xff; this.port_3DA_value = 0xFF; + this.font_page_ab_enabled = false; var io = cpu.io; @@ -377,41 +377,9 @@ function VGAScreen(cpu, bus, screen, vga_memory_size, options) (addr, value) => this.vga_memory_write(addr, value), ); - if(options.use_graphical_text) - { - this.graphical_text = new GraphicalText(this); - } - cpu.devices.pci.register_device(this); } -VGAScreen.prototype.grab_text_content = function(keep_whitespace) -{ - var addr = this.start_address << 1; - const split_screen_row = this.scan_line_to_screen_row(this.line_compare); - const row_offset = Math.max(0, (this.offset_register * 2 - this.max_cols) * 2); - const text_rows = []; - - for(var row = 0; row < this.max_rows; row++) - { - if(row === split_screen_row) - { - addr = 0; - } - - let line = ""; - for(var col = 0; col < this.max_cols; col++, addr += 2) - { - line += String.fromCodePoint(this.vga_memory[addr]); - } - - text_rows.push(keep_whitespace ? line : line.trimEnd()); - addr += row_offset; - } - - return text_rows; -}; - VGAScreen.prototype.get_state = function() { var state = []; @@ -480,6 +448,7 @@ VGAScreen.prototype.get_state = function() state[61] = this.pixel_buffer; state[62] = this.dac_mask; state[63] = this.character_map_select; + state[64] = this.font_page_ab_enabled; return state; }; @@ -550,8 +519,9 @@ VGAScreen.prototype.set_state = function(state) state[61] && this.pixel_buffer.set(state[61]); this.dac_mask = state[62] === undefined ? 0xFF : state[62]; this.character_map_select = state[63] === undefined ? 0 : state[63]; + this.font_page_ab_enabled = state[64] === undefined ? 0 : state[64]; - this.screen.set_mode(this.graphical_mode || !!this.graphical_text); + this.screen.set_mode(this.graphical_mode); if(this.graphical_mode) { @@ -573,7 +543,9 @@ VGAScreen.prototype.set_state = function(state) } else { + this.set_font_bitmap(true); this.set_size_text(this.max_cols, this.max_rows); + this.set_font_page(); this.update_cursor_scanline(); this.update_cursor(); } @@ -676,13 +648,16 @@ VGAScreen.prototype.vga_memory_write = function(addr, value) { this.vga_memory_write_graphical(addr, value); } - else + else if(!(this.plane_write_bm & 0x3)) { - if(!(this.plane_write_bm & 0x3)) + if(this.plane_write_bm & 0x4) { + // write to plane 2 (font-bitmap) this.plane2[addr] = value; - return; } + } + else + { this.vga_memory_write_text_mode(addr, value); } }; @@ -855,15 +830,13 @@ VGAScreen.prototype.apply_bitmask = function(data_dword, bitmask_dword) VGAScreen.prototype.text_mode_redraw = function() { - if(this.graphical_text) - { - return; - } - const split_screen_row = this.scan_line_to_screen_row(this.line_compare); const row_offset = Math.max(0, (this.offset_register * 2 - this.max_cols) * 2); - const blink_flag = this.attribute_mode & 1 << 3; - const bg_color_mask = blink_flag ? 7 : 0xF; + const blink_enabled = this.attribute_mode & 1 << 3; + const fg_color_mask = this.font_page_ab_enabled ? 7 : 0xF; + const bg_color_mask = blink_enabled ? 7 : 0xF; + const FLAG_BLINKING = this.screen.FLAG_BLINKING; + const FLAG_FONT_PAGE_B = this.screen.FLAG_FONT_PAGE_B; let addr = this.start_address << 1; @@ -878,13 +851,15 @@ VGAScreen.prototype.text_mode_redraw = function() { const chr = this.vga_memory[addr]; const color = this.vga_memory[addr | 1]; - const blinking = blink_flag && (color & 1 << 7); + const blinking = blink_enabled && (color & 1 << 7); + const font_page_b = this.font_page_ab_enabled && !(color & 1 << 3); + const flags = (blinking ? FLAG_BLINKING : 0) | (font_page_b ? FLAG_FONT_PAGE_B : 0); this.bus.send("screen-put-char", [row, col, chr]); - this.screen.put_char(row, col, chr, blinking, + this.screen.put_char(row, col, chr, flags, this.vga256_palette[this.dac_mask & this.dac_map[color >> 4 & bg_color_mask]], - this.vga256_palette[this.dac_mask & this.dac_map[color & 0xF]]); + this.vga256_palette[this.dac_mask & this.dac_map[color & fg_color_mask]]); addr += 2; } @@ -935,23 +910,18 @@ VGAScreen.prototype.vga_memory_write_text_mode = function(addr, value) chr = value; color = this.vga_memory[addr | 1]; } - - const blink_flag = this.attribute_mode & 1 << 3; - const blinking = blink_flag && (color & 1 << 7); - const bg_color_mask = blink_flag ? 7 : 0xF; + const blink_enabled = this.attribute_mode & 1 << 3; + const blinking = blink_enabled && (color & 1 << 7); + const font_page_b = this.font_page_ab_enabled && !(color & 1 << 3); + const flags = (blinking ? this.screen.FLAG_BLINKING : 0) | (font_page_b ? this.screen.FLAG_FONT_PAGE_B : 0); + const fg_color_mask = this.font_page_ab_enabled ? 7 : 0xF; + const bg_color_mask = blink_enabled ? 7 : 0xF; this.bus.send("screen-put-char", [row, col, chr]); - if(this.graphical_text) - { - this.graphical_text.invalidate_row(row); - } - else - { - this.screen.put_char(row, col, chr, blinking, - this.vga256_palette[this.dac_mask & this.dac_map[color >> 4 & bg_color_mask]], - this.vga256_palette[this.dac_mask & this.dac_map[color & 0xF]]); - } + this.screen.put_char(row, col, chr, flags, + this.vga256_palette[this.dac_mask & this.dac_map[color >> 4 & bg_color_mask]], + this.vga256_palette[this.dac_mask & this.dac_map[color & fg_color_mask]]); }; VGAScreen.prototype.update_cursor = function() @@ -972,16 +942,9 @@ VGAScreen.prototype.update_cursor = function() } dbg_assert(row >= 0 && col >= 0); - // NOTE: is allowed to be out of bounds - if(this.graphical_text) - { - this.graphical_text.set_cursor_pos(row, col); - } - else - { - this.screen.update_cursor(row, col); - } + // NOTE: is allowed to be out of bounds + this.screen.update_cursor(row, col); }; VGAScreen.prototype.complete_redraw = function() @@ -1174,16 +1137,8 @@ VGAScreen.prototype.set_size_text = function(cols_count, rows_count) this.max_cols = cols_count; this.max_rows = rows_count; + this.screen.set_size_text(cols_count, rows_count); this.bus.send("screen-set-size", [cols_count, rows_count, 0]); - - if(this.graphical_text) - { - this.graphical_text.set_size(rows_count, cols_count); - } - else - { - this.screen.set_size_text(cols_count, rows_count); - } }; VGAScreen.prototype.set_size_graphical = function(width, height, virtual_width, virtual_height, bpp) @@ -1401,15 +1356,7 @@ VGAScreen.prototype.update_cursor_scanline = function() const start = Math.min(max, this.cursor_scanline_start & 0x1F); const end = Math.min(max, this.cursor_scanline_end & 0x1F); const visible = !disabled && start < end; - - if(this.graphical_text) - { - this.graphical_text.set_cursor_attr(start, end, visible); - } - else - { - this.screen.update_cursor_scanline(start, end, visible); - } + this.screen.update_cursor_scanline(start, end, visible); }; /** @@ -1460,7 +1407,7 @@ VGAScreen.prototype.port3C0_write = function(value) if(!this.svga_enabled && this.graphical_mode !== is_graphical) { this.graphical_mode = is_graphical; - this.screen.set_mode(this.graphical_mode || !!this.graphical_text); + this.screen.set_mode(this.graphical_mode); } if((previous_mode ^ value) & 0x40) @@ -1473,6 +1420,8 @@ VGAScreen.prototype.port3C0_write = function(value) // Data stored in image buffer are invalidated this.complete_redraw(); + + this.set_font_bitmap(false); } break; case 0x12: @@ -1588,24 +1537,25 @@ VGAScreen.prototype.port3C5_write = function(value) // Screen disable bit modified this.update_layers(); } + this.set_font_bitmap(false); break; case 0x02: dbg_log("plane write mask: " + h(value), LOG_VGA); var previous_plane_write_bm = this.plane_write_bm; this.plane_write_bm = value; - if(this.graphical_text && previous_plane_write_bm !== 0xf && (previous_plane_write_bm & 0x4) && !(this.plane_write_bm & 0x4)) + if(!this.graphical_mode && previous_plane_write_bm & 0x4 && !(this.plane_write_bm & 0x4)) { - // End of font plane 2 write access (initial value of plane_write_bm assumed to be 0xf) - this.graphical_text.invalidate_font_shape(); + // End of font plane 2 write access + this.set_font_bitmap(true); } break; case 0x03: dbg_log("character map select: " + h(value), LOG_VGA); var previous_character_map_select = this.character_map_select; this.character_map_select = value; - if(this.graphical_text && previous_character_map_select !== this.character_map_select) + if(!this.graphical_mode && previous_character_map_select !== value) { - this.graphical_text.set_character_map(this.character_map_select); + this.set_font_page(); } break; case 0x04: @@ -1640,7 +1590,11 @@ VGAScreen.prototype.port3C5_read = function() VGAScreen.prototype.port3C6_write = function(data) { - this.dac_mask = data; + if(this.dac_mask !== data) + { + this.dac_mask = data; + this.complete_redraw(); + } }; VGAScreen.prototype.port3C6_read = function() @@ -1923,6 +1877,8 @@ VGAScreen.prototype.port3D5_write = function(value) this.update_cursor_scanline(); this.update_layers(); + + this.set_font_bitmap(false); break; case 0xA: dbg_log("3D5 / cursor scanline start write: " + h(value), LOG_VGA); @@ -2495,19 +2451,9 @@ VGAScreen.prototype.screen_fill_buffer = function() if(!this.graphical_mode) { // text mode - if(this.graphical_text) - { - const image_data = this.graphical_text.render(); - this.screen.update_buffer([{ - image_data: image_data, - screen_x: 0, - screen_y: 0, - buffer_x: 0, - buffer_y: 0, - buffer_width: image_data.width, - buffer_height: image_data.height - }]); - } + // Update retrace behaviour anyway - programs waiting for signal before + // changing to graphical mode + this.update_vertical_retrace(); return; } @@ -2570,3 +2516,35 @@ VGAScreen.prototype.screen_fill_buffer = function() this.reset_diffs(); this.update_vertical_retrace(); }; + +VGAScreen.prototype.set_font_bitmap = function(font_plane_dirty) +{ + const height = this.max_scan_line & 0x1f; + if(height && !this.graphical_mode) + { + const width_dbl = !!(this.clocking_mode & 0x08); + const width_9px = !width_dbl && !(this.clocking_mode & 0x01); + const copy_8th_col = !!(this.attribute_mode & 0x04); + this.screen.set_font_bitmap( + height + 1, // int height, font height 1..32px + width_9px, // bool width_9px, True: font width 9px, else 8px + width_dbl, // bool width_dbl, True: font width 16px (overrides width_9px) + copy_8th_col, // bool copy_8th_col, True: duplicate 8th into 9th column in ASCII chars 0xC0-0xDF + this.plane2, // Uint8Array font_bitmap[64k], static + font_plane_dirty // bool bitmap_changed, True: content of this.plane2 has changed + ); + } +}; + +VGAScreen.prototype.set_font_page = function() +{ + // bits 2, 3 and 5 (LSB to MSB): VGA font page index of font A + // bits 0, 1 and 4: VGA font page index of font B + // linear_index_map[] maps VGA's non-liner font page index to linear index + const linear_index_map = [0, 2, 4, 6, 1, 3, 5, 7]; + const vga_index_A = ((this.character_map_select & 0b1100) >> 2) | ((this.character_map_select & 0b100000) >> 3); + const vga_index_B = (this.character_map_select & 0b11) | ((this.character_map_select & 0b10000) >> 2); + this.font_page_ab_enabled = vga_index_A !== vga_index_B; + this.screen.set_font_page(linear_index_map[vga_index_A], linear_index_map[vga_index_B]); + this.complete_redraw(); +}; diff --git a/src/vga_text.js b/src/vga_text.js deleted file mode 100644 index 5b5949b038..0000000000 --- a/src/vga_text.js +++ /dev/null @@ -1,628 +0,0 @@ -/* -vga_text.js - -Renders text to image buffer using VGA fonts and attributes. -*/ -"use strict"; - -/** - * @constructor - * @param {VGAScreen} vga - */ -function GraphicalText(vga) -{ - this.vga = vga; - - /** - * Number of text columns - * @type {number} - */ - this.txt_width = 80; - - /** - * Number of text rows - * @type {number} - */ - this.txt_height = 25; - - /** - * If true then at least one row in txt_row_dirty is marked as modified - * @type{number} - */ - this.txt_dirty = 0; - - /** - * One bool per row, row was modified if its entry is != 0 - */ - this.txt_row_dirty = new Uint8Array(this.txt_height); - - /** - * Font bitmaps in VGA memory were changed if true - * @type{boolean} - */ - this.font_data_dirty = false; - - /** - * Font width in pixel (8, 9 or 16) - * @type {number} - */ - this.font_width = 9; - - /** - * Font height in pixel (0...32) - * @type {number} - */ - this.font_height = 16; - - /** - * Duplicate 8th to 9th column in horizontal line drawing characters if true (Line Graphics Enable) - * @type{boolean} - */ - this.font_lge = false; - - /** - * Flat bitmap of 8 fonts, array of size: 8 * 256 * font_width * font_height - * @type{Uint8ClampedArray} - */ - this.font_bitmap = new Uint8ClampedArray(8 * 256 * this.font_width * this.font_height); - - /** - * True: blink when msb (0x80) of text attribute is set (8 background colors) - * False: msb selects background intensity (16 background colors) - * @type{boolean} - */ - this.font_blink_enabled = false; - - /** - * Active index (0...7) of font A - * @type {number} - */ - this.font_index_A = 0; - - /** - * Active index (0...7) of font B (TODO) - * @type {number} - */ - this.font_index_B = 0; - - /** - * If true then cursor_enabled_latch, cursor_top_latch and cursor_bottom_latch were overwritten since last call to render(). - * @type{boolean} - */ - this.cursor_attr_dirty = false; - - /** - * Latest value for cursor_enabled if cursor_attr_dirty is true - * @type{boolean} - */ - this.cursor_enabled_latch = false; - - /** - * Latest value for cursor_top_latch if cursor_attr_dirty is true - * @type {number} - */ - this.cursor_top_latch = 0; - - /** - * Latest value for cursor_bottom_latch if cursor_attr_dirty is true - * @type {number} - */ - this.cursor_bottom_latch = 0; - - /** - * If true then cursor_row_latch and cursor_col_latch were overwritten since last call to render(). - * @type{boolean} - */ - this.cursor_pos_dirty = false; - - /** - * Latest value for cursor_row if cursor_pos_dirty is true - * @type {number} - */ - this.cursor_row_latch = 0; - - /** - * Latest value for cursor_col if cursor_pos_dirty is true - * @type {number} - */ - this.cursor_col_latch = 0; - - /** - * Emulate cursor if true, else disable cursor - * @type{boolean} - */ - this.cursor_enabled = false; - - /** - * Cursor position's row (0...txt_height-1) - * @type {number} - */ - this.cursor_row = 0; - - /** - * Cursor position's column (0...txt_width-1) - * @type {number} - */ - this.cursor_col = 0; - - /** - * Cursor box's top scanline (0...font_height) - * @type {number} - */ - this.cursor_top = 0; - - /** - * Cursor box's bottom scanline (0...font_height, inclusive) - * @type {number} - */ - this.cursor_bottom = 0; - - /** - * Tracked value of register vga.attribute_mode - * @type {number} - */ - this.vga_attribute_mode = 0; - - /** - * Tracked value of register vga.clocking_mode - * @type {number} - */ - this.vga_clocking_mode = 0; - - /** - * Tracked value of register vga.max_scan_line - * @type {number} - */ - this.vga_max_scan_line = 0; - - /** - * Width of graphics canvas in pixel (txt_width * font_width) - * @type {number} - */ - this.gfx_width = this.txt_width * this.font_width; - - /** - * Height of graphics canvas in pixel (txt_height * font_height) - * @type {number} - */ - this.gfx_height = this.txt_height * this.font_height; - - /** - * Local screen bitmap buffer, array of size: gfx_width * gfx_height * 4 - * @type{Uint8ClampedArray} - */ - this.gfx_data = new Uint8ClampedArray(this.gfx_width * this.gfx_height * 4); - - /** - * Image container of local screen bitmap buffer gfx_data - * @type{ImageData} - */ - this.image_data = new ImageData(this.gfx_data, this.gfx_width, this.gfx_height); - - /** - * Show cursor and blinking text now if true (controlled by framerate counter) - * @type{boolean} - */ - this.blink_visible = false; - - /** - * Frame counter to control blink rate of type Uint32 - * @type {number} - */ - this.frame_count = 0; -} - -GraphicalText.prototype.rebuild_font_bitmap = function(width_9px, width_double) -{ - const font_height = this.font_height; - const font_lge = this.font_lge; - const src_bitmap = this.vga.plane2; - const dst_bitmap = new Uint8ClampedArray(8 * 256 * this.font_width * font_height); - const vga_inc_chr = 32 - font_height; - - let i_dst = 0; - const copy_bit = width_double ? - function(value) - { - dst_bitmap[i_dst++] = value; - dst_bitmap[i_dst++] = value; - } : - function(value) - { - dst_bitmap[i_dst++] = value; - }; - - let i_src = 0; - for(let i_font = 0; i_font < 8; ++i_font) - { - for(let i_chr = 0; i_chr < 256; ++i_chr, i_src += vga_inc_chr) - { - for(let i_line = 0; i_line < font_height; ++i_line) - { - const line_bits = src_bitmap[i_src++]; - for(let i_bit = 0x80; i_bit > 0; i_bit >>= 1) - { - copy_bit(line_bits & i_bit ? 1 : 0); - } - if(width_9px) - { - copy_bit(font_lge && i_chr >= 0xC0 && i_chr <= 0xDF && line_bits & 1 ? 1 : 0); - } - } - } - } - - return dst_bitmap; -}; - -GraphicalText.prototype.resize_canvas = function() -{ - this.txt_dirty = 1; - this.txt_row_dirty.fill(1); -}; - -GraphicalText.prototype.rebuild_image_data = function() -{ - const gfx_size = this.gfx_width * this.gfx_height * 4; - const gfx_data = new Uint8ClampedArray(gfx_size); - for(let i = 3; i < gfx_size; i += 4) - { - gfx_data[i] = 0xff; - } - this.gfx_data = gfx_data; - this.image_data = new ImageData(this.gfx_data, this.gfx_width, this.gfx_height); - this.resize_canvas(); -}; - -GraphicalText.prototype.mark_blinking_rows_dirty = function() -{ - const vga_memory = this.vga.vga_memory; - const txt_row_dirty = this.txt_row_dirty; - const txt_width = this.txt_width; - const txt_height = this.txt_height; - const txt_row_size = txt_width * 2; - const txt_row_step = Math.max(0, (this.vga.offset_register * 2 - txt_width) * 2); - const split_screen_row = this.vga.scan_line_to_screen_row(this.vga.line_compare); - let row, col, txt_i = this.vga.start_address << 1; - - for(row = 0; row < txt_height; ++row, txt_i += txt_row_step) - { - if(row === split_screen_row) - { - txt_i = 0; - } - - if(txt_row_dirty[row]) - { - txt_i += txt_row_size; - continue; - } - - for(col = 0; col < txt_width; ++col, txt_i += 2) - { - if(vga_memory[txt_i | 1] & 0x80) - { - txt_row_dirty[row] = this.txt_dirty = 1; - txt_i += txt_row_size - col * 2; - break; - } - } - } -}; - -GraphicalText.prototype.render_dirty_rows = function() -{ - const vga = this.vga; - const vga_memory = vga.vga_memory; - const txt_width = this.txt_width; - const txt_height = this.txt_height; - const txt_row_dirty = this.txt_row_dirty; - const gfx_data = this.gfx_data; - const font_bitmap = this.font_bitmap; - const font_size = this.font_width * this.font_height; - const font_A_offset = this.font_index_A * 256; - const font_B_offset = this.font_index_B * 256; - const font_AB_enabled = font_A_offset !== font_B_offset; - const font_blink_enabled = this.font_blink_enabled; - //const blink_visible = this.blink_visible; - const blink_visible = true; - const cursor_visible = this.cursor_enabled && blink_visible; - const cursor_top = this.cursor_top; - const cursor_height = this.cursor_bottom - cursor_top + 1; - - const split_screen_row = vga.scan_line_to_screen_row(vga.line_compare); - const bg_color_mask = font_blink_enabled ? 0x7 : 0xF; - const palette = new Int32Array(16); - for(let i = 0; i < 16; ++i) - { - palette[i] = vga.vga256_palette[vga.dac_mask & vga.dac_map[i]]; - } - - const txt_row_size = txt_width * 2; - const txt_row_step = Math.max(0, (vga.offset_register * 2 - txt_width) * 2); - - const gfx_col_size = this.font_width * 4; // column size in gfx_data (tuple of 4 RGBA items) - const gfx_line_size = this.gfx_width * 4; // line size in gfx_data - const gfx_row_size = gfx_line_size * this.font_height; // row size in gfx_data - const gfx_col_step = (this.font_width - this.font_height * this.gfx_width) * 4; // move from end of current column to start of next in gfx_data - const gfx_line_step = (this.gfx_width - this.font_width) * 4; // move forward to start of column's next line in gfx_data - - // int, current cursor linear position in canvas coordinates (top left of row/col) - const cursor_gfx_i = (this.cursor_row * this.gfx_width * this.font_height + this.cursor_col * this.font_width) * 4; - - let txt_i, chr, chr_attr, chr_bg_rgba, chr_fg_rgba, chr_blinking, chr_font_ofs; - let fg, bg, fg_r=0, fg_g=0, fg_b=0, bg_r=0, bg_g=0, bg_b=0; - let gfx_i, gfx_end_y, gfx_end_x, glyph_i; - let draw_cursor, gfx_ic; - let row, col; - - txt_i = vga.start_address << 1; - - for(row = 0; row < txt_height; ++row, txt_i += txt_row_step) - { - if(row === split_screen_row) - { - txt_i = 0; - } - - if(! txt_row_dirty[row]) - { - txt_i += txt_row_size; - continue; - } - - gfx_i = row * gfx_row_size; - - for(col = 0; col < txt_width; ++col, txt_i += 2, gfx_i += gfx_col_step) - { - chr = vga_memory[txt_i]; - chr_attr = vga_memory[txt_i | 1]; - chr_blinking = font_blink_enabled && chr_attr & 0x80; - chr_font_ofs = font_AB_enabled ? (chr_attr & 0x8 ? font_A_offset : font_B_offset) : font_A_offset; - chr_bg_rgba = palette[chr_attr >> 4 & bg_color_mask]; - chr_fg_rgba = palette[chr_attr & 0xF]; - - if(bg !== chr_bg_rgba) - { - bg = chr_bg_rgba; - bg_r = bg >> 16; - bg_g = (bg >> 8) & 0xff; - bg_b = bg & 0xff; - } - - if(chr_blinking && ! blink_visible) - { - if(fg !== bg) { - fg = bg; - fg_r = bg_r; - fg_g = bg_g; - fg_b = bg_b; - } - } - else if(fg !== chr_fg_rgba) - { - fg = chr_fg_rgba; - fg_r = fg >> 16; - fg_g = (fg >> 8) & 0xff; - fg_b = fg & 0xff; - } - - draw_cursor = cursor_visible && cursor_gfx_i === gfx_i; - - glyph_i = (chr_font_ofs + chr) * font_size; - - gfx_end_y = gfx_i + gfx_row_size; - for(; gfx_i < gfx_end_y; gfx_i += gfx_line_step) - { - gfx_end_x = gfx_i + gfx_col_size; - for(; gfx_i < gfx_end_x; gfx_i += 4) - { - if(font_bitmap[glyph_i++]) - { - gfx_data[gfx_i] = fg_r; - gfx_data[gfx_i+1] = fg_g; - gfx_data[gfx_i+2] = fg_b; - } - else - { - gfx_data[gfx_i] = bg_r; - gfx_data[gfx_i+1] = bg_g; - gfx_data[gfx_i+2] = bg_b; - } - } - } - - if(draw_cursor) - { - gfx_ic = cursor_gfx_i + cursor_top * gfx_line_size; - gfx_end_y = gfx_ic + cursor_height * gfx_line_size; - for(; gfx_ic < gfx_end_y; gfx_ic += gfx_line_step) - { - gfx_end_x = gfx_ic + gfx_col_size; - for(; gfx_ic < gfx_end_x; gfx_ic += 4) - { - gfx_data[gfx_ic] = fg_r; - gfx_data[gfx_ic+1] = fg_g; - gfx_data[gfx_ic+2] = fg_b; - } - } - } - } - } -}; - -// -// Public methods -// - -GraphicalText.prototype.mark_dirty = function() -{ - this.txt_row_dirty.fill(1); - this.txt_dirty = 1; -}; - -GraphicalText.prototype.invalidate_row = function(row) -{ - if(row >= 0 && row < this.txt_height) - { - this.txt_row_dirty[row] = this.txt_dirty = 1; - } -}; - -GraphicalText.prototype.invalidate_font_shape = function() -{ - this.font_data_dirty = true; -}; - -GraphicalText.prototype.set_size = function(rows, cols) -{ - if(rows > 0 && rows < 256 && cols > 0 && cols < 256) - { - this.txt_width = cols; - this.txt_height = rows; - - this.gfx_width = this.txt_width * this.font_width; - this.gfx_height = this.txt_height * this.font_height; - - this.txt_row_dirty = new Uint8Array(this.txt_height); - this.vga.screen.set_size_graphical(this.gfx_width, this.gfx_height, this.gfx_width, this.gfx_height); - this.mark_dirty(); - this.rebuild_image_data(); - } -}; - -GraphicalText.prototype.set_character_map = function(char_map_select) -{ - // bits 2, 3 and 5 (LSB to MSB): VGA font page index of font A - // bits 0, 1 and 4: VGA font page index of font B - // linear_index_map[] maps VGA's non-liner font page index to linear index - const linear_index_map = [0, 2, 4, 6, 1, 3, 5, 7]; - const vga_index_A = ((char_map_select & 0b1100) >> 2) | ((char_map_select & 0b100000) >> 3); - const vga_index_B = (char_map_select & 0b11) | ((char_map_select & 0b10000) >> 2); - const font_index_A = linear_index_map[vga_index_A]; - const font_index_B = linear_index_map[vga_index_B]; - - if(this.font_index_A !== font_index_A || this.font_index_B !== font_index_B) - { - this.font_index_A = font_index_A; - this.font_index_B = font_index_B; - this.mark_dirty(); - } -}; - -GraphicalText.prototype.set_cursor_pos = function(row, col) -{ - this.cursor_pos_dirty = true; - this.cursor_row_latch = row; - this.cursor_col_latch = col; -}; - -GraphicalText.prototype.set_cursor_attr = function(start, end, visible) -{ - this.cursor_attr_dirty = true; - this.cursor_enabled_latch = !! visible; - this.cursor_top_latch = start; - this.cursor_bottom_latch = end; -}; - -GraphicalText.prototype.render = function() -{ - // increment Uint32 frame counter - this.frame_count = (this.frame_count + 1) >>> 0; - - // apply changes to font_width, font_height, font_lge, font_bitmap and font_blink_enabled - const curr_clocking_mode = this.vga.clocking_mode & 0b00001001; - const curr_attribute_mode = this.vga.attribute_mode & 0b00001100; - const curr_max_scan_line = this.vga.max_scan_line & 0b10011111; - if(this.font_data_dirty || - this.vga_clocking_mode !== curr_clocking_mode || - this.vga_attribute_mode !== curr_attribute_mode || - this.vga_max_scan_line !== curr_max_scan_line) - { - const width_9px = ! (curr_clocking_mode & 0x01); - const width_double = !! (curr_clocking_mode & 0x08); - const curr_font_width = (width_9px ? 9 : 8) * (width_double ? 2 : 1); - const curr_font_blink_enabled = !! (curr_attribute_mode & 0b00001000); - const curr_font_lge = !! (curr_attribute_mode & 0b00000100); - const curr_font_height = (curr_max_scan_line & 0b00011111) + 1; - - const font_data_changed = this.font_data_dirty || this.font_lge !== curr_font_lge; - const font_size_changed = this.font_width !== curr_font_width || this.font_height !== curr_font_height; - - this.font_data_dirty = false; - this.font_width = curr_font_width; - this.font_height = curr_font_height; - this.font_blink_enabled = curr_font_blink_enabled; - this.font_lge = curr_font_lge; - - this.vga_clocking_mode = curr_clocking_mode; - this.vga_attribute_mode = curr_attribute_mode; - this.vga_max_scan_line = curr_max_scan_line; - - if(font_data_changed || font_size_changed) - { - if(font_size_changed) - { - this.gfx_width = this.txt_width * this.font_width; - this.gfx_height = this.txt_height * this.font_height; - this.rebuild_image_data(); - } - this.font_bitmap = this.rebuild_font_bitmap(width_9px, width_double); - } - this.mark_dirty(); - } - - // apply changes to cursor position - if(this.cursor_pos_dirty) - { - this.cursor_pos_dirty = false; - this.cursor_row_latch = Math.min(this.cursor_row_latch, this.txt_height-1); - this.cursor_col_latch = Math.min(this.cursor_col_latch, this.txt_width-1); - if(this.cursor_row !== this.cursor_row_latch || this.cursor_col !== this.cursor_col_latch) - { - this.txt_row_dirty[this.cursor_row] = this.txt_row_dirty[this.cursor_row_latch] = this.txt_dirty = 1; - this.cursor_row = this.cursor_row_latch; - this.cursor_col = this.cursor_col_latch; - } - } - - // apply changes to cursor_enabled, cursor_top and cursor_bottom - if(this.cursor_attr_dirty) - { - this.cursor_attr_dirty = false; - if(this.cursor_enabled !== this.cursor_enabled_latch || - this.cursor_top !== this.cursor_top_latch || - this.cursor_bottom !== this.cursor_bottom_latch) - { - this.cursor_enabled = this.cursor_enabled_latch; - this.cursor_top = this.cursor_top_latch; - this.cursor_bottom = this.cursor_bottom_latch; - this.txt_row_dirty[this.cursor_row] = this.txt_dirty = 1; - } - } - - // toggle cursor and blinking character visibility at a frequency of ~3.75hz (every 16th frame at 60fps) - // TODO: make framerate independant - //if(this.frame_count % 16 === 0) - //{ - // this.blink_visible = ! this.blink_visible; - // if(this.font_blink_enabled) - // { - // this.mark_blinking_rows_dirty(); - // } - // if(this.cursor_enabled) - // { - // this.txt_row_dirty[this.cursor_row] = this.txt_dirty = 1; - // } - //} - - // render changed rows - if(this.txt_dirty) - { - this.render_dirty_rows(); - this.txt_dirty = 0; - this.txt_row_dirty.fill(0); - } - - return this.image_data; -}; diff --git a/src/virtio_net.js b/src/virtio_net.js index eb972986a3..29b4152468 100644 --- a/src/virtio_net.js +++ b/src/virtio_net.js @@ -229,8 +229,8 @@ VirtioNet.prototype.set_state = function(state) this.virtio.set_state(state[0]); }; -VirtioNet.prototype.Reset = function() { - +VirtioNet.prototype.reset = function() { + this.virtio.reset(); }; VirtioNet.prototype.Send = function (queue_id, bufchain, blob) diff --git a/tests/api/clean-shutdown.js b/tests/api/clean-shutdown.js index 5e011f9cfb..f645d822c6 100755 --- a/tests/api/clean-shutdown.js +++ b/tests/api/clean-shutdown.js @@ -1,7 +1,7 @@ #!/usr/bin/env node "use strict"; -// This test checks that calling emulator.stop() will remove all event +// This test checks that calling emulator.destroy() will remove all event // listeners, so that the nodejs process cleanly and automatically exits. const TEST_RELEASE_BUILD = +process.env.TEST_RELEASE_BUILD; @@ -28,6 +28,6 @@ const emulator = new V86(config); setTimeout(function() { console.error("Calling stop()"); - emulator.stop(); + emulator.destroy(); console.error("Called stop()"); }, 3000); diff --git a/tests/api/floppy-insert-eject.js b/tests/api/floppy-insert-eject.js index 12246c87ac..f6536e6c4f 100755 --- a/tests/api/floppy-insert-eject.js +++ b/tests/api/floppy-insert-eject.js @@ -3,6 +3,7 @@ const TEST_RELEASE_BUILD = +process.env.TEST_RELEASE_BUILD; +const pause = require("timers/promises").setTimeout; const fs = require("fs"); var V86 = require(`../../build/${TEST_RELEASE_BUILD ? "libv86" : "libv86-debug"}.js`).V86; @@ -16,23 +17,34 @@ const emulator = new V86({ autostart: true, memory_size: 32 * 1024 * 1024, filesystem: {}, - log_level: 0, + log_level: 3, disable_jit: +process.env.DISABLE_JIT, }); +//const interval = setInterval(() => { +// console.warn(emulator.screen_adapter.get_text_screen()); +//}, 1000); + const timeout = setTimeout(() => { + console.warn(emulator.screen_adapter.get_text_screen()); throw new Error("Timeout"); }, 60 * 1000); setTimeout(async () => { await emulator.wait_until_vga_screen_contains("C:\\> "); + console.log("Got C:\\>"); + await pause(1000); emulator.keyboard_send_text("dir A:\n"); await emulator.wait_until_vga_screen_contains("Abort, Retry, Fail?"); + console.log("Got Abort, Retry, Fail?"); + await pause(1000); emulator.keyboard_send_text("F"); emulator.set_fda({ url: __dirname + "/../../images/freedos722.img" }); emulator.keyboard_send_text("dir A:\n"); await emulator.wait_until_vga_screen_contains("FDOS "); - emulator.stop(); + console.log("Got FDOS"); + emulator.destroy(); clearTimeout(timeout); + //clearInterval(interval); }, 1000); diff --git a/tests/api/reboot.js b/tests/api/reboot.js new file mode 100755 index 0000000000..03b454d86c --- /dev/null +++ b/tests/api/reboot.js @@ -0,0 +1,61 @@ +#!/usr/bin/env node +"use strict"; + +const TEST_RELEASE_BUILD = +process.env.TEST_RELEASE_BUILD; + +const fs = require("fs"); +var V86 = require(`../../build/${TEST_RELEASE_BUILD ? "libv86" : "libv86-debug"}.js`).V86; + +process.on("unhandledRejection", exn => { throw exn; }); + +const config = { + bios: { url: __dirname + "/../../bios/seabios.bin" }, + vga_bios: { url: __dirname + "/../../bios/vgabios.bin" }, + cdrom: { url: __dirname + "/../../images/linux4.iso", async: true }, + net_device: { + relay_url: "fetch", + type: "virtio", + }, + autostart: true, + memory_size: 32 * 1024 * 1024, + filesystem: {}, + virtio_console: true, + log_level: 0, + disable_jit: +process.env.DISABLE_JIT, +}; + +const emulator = new V86(config); + +let did_reboot = false; +let serial_text = ""; + +const timeout = setTimeout(() => { + console.log(serial_data); + throw new Error("Timeout"); +}, 120 * 1000); + +emulator.add_listener("serial0-output-byte", function(byte) +{ + var chr = String.fromCharCode(byte); + serial_text += chr; + + if(did_reboot) + { + if(serial_text.endsWith("Files send via emulator appear in /mnt/")) + { + console.log("Ok"); + emulator.destroy(); + clearTimeout(timeout); + } + } + else + { + if(serial_text.endsWith("~% ")) + { + console.log("rebooting"); + emulator.serial0_send("reboot\n"); + serial_text = ""; + did_reboot = true; + } + } +}); diff --git a/tests/api/reset.js b/tests/api/reset.js index d4657dd8cf..630d2abc2b 100755 --- a/tests/api/reset.js +++ b/tests/api/reset.js @@ -36,7 +36,7 @@ emulator.add_listener("serial0-output-byte", function(byte) serial_text = ""; if(did_restart) { console.log("Ok"); - emulator.stop(); + emulator.destroy(); } else { console.log("Calling restart()"); diff --git a/tests/api/serial.js b/tests/api/serial.js index 88ab4c3a79..d9e1e45e37 100755 --- a/tests/api/serial.js +++ b/tests/api/serial.js @@ -49,6 +49,6 @@ emulator.add_listener("serial0-output-byte", function(byte) assert("da1fb5b421123c58080a59832675632505b8c139a8d7ecd1c31591ca5c65cea6" === hash.digest("hex")); console.log("ok"); clearTimeout(timeout); - emulator.stop(); + emulator.destroy(); } }); diff --git a/tests/api/state.js b/tests/api/state.js index 91a6180fe7..152e213657 100755 --- a/tests/api/state.js +++ b/tests/api/state.js @@ -87,7 +87,7 @@ async function run_test(name, config, done) } console.log("Done: %s", name); - emulator.stop(); + emulator.destroy(); } (async function() { diff --git a/tests/benchmark/arch-bytemark.js b/tests/benchmark/arch-bytemark.js index 887d6c91c8..a9d13c9b0d 100755 --- a/tests/benchmark/arch-bytemark.js +++ b/tests/benchmark/arch-bytemark.js @@ -69,7 +69,7 @@ emulator.add_listener("serial0-output-byte", function(byte) if(line === "* Trademarks are property of their respective holder.") { - emulator.stop(); + emulator.destroy(); if(BENCH_COLLECT_STATS) { diff --git a/tests/benchmark/arch-python.js b/tests/benchmark/arch-python.js index c68e6120be..1d16a10464 100755 --- a/tests/benchmark/arch-python.js +++ b/tests/benchmark/arch-python.js @@ -54,7 +54,7 @@ emulator.add_listener("serial0-output-byte", function(byte) if(line.startsWith("sys")) { - emulator.stop(); + emulator.destroy(); if(BENCH_COLLECT_STATS) { diff --git a/tests/benchmark/linux-boot.js b/tests/benchmark/linux-boot.js index 07e6c1b602..81529d7676 100755 --- a/tests/benchmark/linux-boot.js +++ b/tests/benchmark/linux-boot.js @@ -71,7 +71,7 @@ emulator.add_listener("serial0-output-byte", function(byte) const end_time = Date.now(); const elapsed = end_time - start_time; console.log("Done in %dms", elapsed); - emulator.stop(); + emulator.destroy(); if(BENCH_COLLECT_STATS) { diff --git a/tests/devices/fetch_network.js b/tests/devices/fetch_network.js index 8f98bb78ca..373007c624 100755 --- a/tests/devices/fetch_network.js +++ b/tests/devices/fetch_network.js @@ -124,7 +124,45 @@ const tests = assert(/This domain is for use in illustrative examples in documents/.test(capture), "got example.org text"); }, }, - + { + name: "Forbidden character in header name", + start: () => + { + emulator.serial0_send("wget --header='test.v86: 123' -T 10 -O - test.domain\n"); + emulator.serial0_send("echo -e done\\\\tincorrect header name\n"); + }, + end_trigger: "done\tincorrect header name", + end: (capture) => + { + assert(/400 Bad Request/.test(capture), "got error 400"); + }, + }, + { + name: "Empty header value", + start: () => + { + emulator.serial0_send("wget --header='test:' -T 10 -O - test.domain\n"); + emulator.serial0_send("echo -e done\\\\tempty header value\n"); + }, + end_trigger: "done\tempty header value", + end: (capture) => + { + assert(/400 Bad Request/.test(capture), "got error 400"); + }, + }, + { + name: "Header without separator", + start: () => + { + emulator.serial0_send("wget --spider --header='testheader' -T 10 -O - test.domain\n"); + emulator.serial0_send("echo -e done\\\\theader without colon\n"); + }, + end_trigger: "done\theader without colon", + end: (capture) => + { + assert(/400 Bad Request/.test(capture), "got error 400"); + }, + } ]; const emulator = new V86({ @@ -227,7 +265,6 @@ emulator.add_listener("serial0-output-byte", function(byte) if(test_num >= tests.length) { - emulator.stop(); emulator.destroy(); console.log("Tests finished."); diff --git a/tests/devices/virtio_console.js b/tests/devices/virtio_console.js index 6d88a1a5ba..beec4da408 100755 --- a/tests/devices/virtio_console.js +++ b/tests/devices/virtio_console.js @@ -59,7 +59,7 @@ emulator.add_listener("serial0-output-byte", function(byte) if(line.endsWith("pong")) { console.log("\nTest passed"); - emulator.stop(); + emulator.destroy(); } }); diff --git a/tests/devices/wisp_network.js b/tests/devices/wisp_network.js index b20af10fda..f8bad868e8 100755 --- a/tests/devices/wisp_network.js +++ b/tests/devices/wisp_network.js @@ -162,7 +162,6 @@ emulator.add_listener("serial0-output-byte", function(byte) if(test_num >= tests.length) { - emulator.stop(); emulator.destroy(); console.log("Tests finished."); diff --git a/tests/expect/tests/call-ret.wast b/tests/expect/tests/call-ret.wast index c59daa3bc7..b26df4a9ac 100644 --- a/tests/expect/tests/call-ret.wast +++ b/tests/expect/tests/call-ret.wast @@ -20,10 +20,10 @@ (type $t18 (func (param i32 i64 i32))) (type $t19 (func (param i32 i64 i32) (result i32))) (type $t20 (func (param i32 i64 i64 i32) (result i32))) + (import "e" "instr_F4" (func $e.instr_F4 (type $t0))) (import "e" "safe_write32_slow_jit" (func $e.safe_write32_slow_jit (type $t16))) (import "e" "safe_read32s_slow_jit" (func $e.safe_read32s_slow_jit (type $t7))) (import "e" "jit_find_cache_entry_in_page" (func $e.jit_find_cache_entry_in_page (type $t16))) - (import "e" "instr_F4" (func $e.instr_F4 (type $t0))) (import "e" "trigger_fault_end_jit" (func $e.trigger_fault_end_jit (type $t0))) (import "e" "m" (memory {normalised output})) (func $f (export "f") (type $t1) (param $p0 i32) @@ -72,234 +72,234 @@ (i32.add (get_local $l8) (i32.const 1))) - (set_local $l9 - (i32.sub - (i32.or - (i32.and - (i32.load - (i32.const 556)) - (i32.const -4096)) - (i32.const 5)) - (i32.load - (i32.const 740)))) - (set_local $l11 - (i32.add - (tee_local $l10 - (i32.sub - (get_local $l4) - (i32.const 4))) - (i32.load - (i32.const 744)))) - (block $B6 - (br_if $B6 - (i32.and - (i32.eq - (i32.and - (tee_local $l12 - (i32.load offset={normalised output} - (i32.shl - (i32.shr_u - (get_local $l11) - (i32.const 12)) - (i32.const 2)))) - (i32.const 4075)) - (i32.const 1)) - (i32.le_s - (i32.and - (get_local $l11) - (i32.const 4095)) - (i32.const 4092)))) - (br_if $B1 - (i32.and - (tee_local $l12 - (call $e.safe_write32_slow_jit - (get_local $l11) - (get_local $l9) - (i32.const 0))) - (i32.const 1)))) - (i32.store align=1 - (i32.xor + (i32.store + (i32.const 560) + (i32.or (i32.and - (get_local $l12) + (i32.load + (i32.const 556)) (i32.const -4096)) - (get_local $l11)) - (get_local $l9)) - (set_local $l4 - (get_local $l10)) - (set_local $l8 - (i32.add - (get_local $l8) - (i32.const 2))) + (i32.const 5))) (i32.store - (i32.const 120) + (i32.const 556) (i32.or (i32.and (i32.load - (i32.const 120)) - (i32.const -2)) - (if $I7 (result i32) - (i32.and - (tee_local $l9 - (i32.load - (i32.const 100))) - (i32.const 1)) - (then - (set_local $l9 - (i32.shr_s - (get_local $l9) - (i32.const 31))) - (i32.lt_u - (i32.xor - (i32.load - (i32.const 112)) - (get_local $l9)) - (i32.xor - (i32.load - (i32.const 104)) - (get_local $l9)))) - (else - (i32.and - (i32.load - (i32.const 120)) - (i32.const 1)))))) + (i32.const 556)) + (i32.const -4096)) + (i32.const 6))) (i32.store - (i32.const 104) + (i32.const 64) (get_local $l0)) - (set_local $l0 - (i32.add - (get_local $l0) - (i32.const 1))) (i32.store - (i32.const 112) - (get_local $l0)) - (i64.store - (i32.const 96) - (i64.const 9706626088991)) - (i32.const 0) - (set_local $l9 - (i32.add - (get_local $l4) - (i32.load - (i32.const 744)))) - (block $B8 - (br_if $B8 - (i32.and - (i32.eq - (i32.and - (tee_local $l10 - (i32.load offset={normalised output} - (i32.shl - (i32.shr_u - (get_local $l9) - (i32.const 12)) - (i32.const 2)))) - (i32.const 4041)) - (i32.const 1)) - (i32.le_s - (i32.and - (get_local $l9) - (i32.const 4095)) - (i32.const 4092)))) - (br_if $B1 - (i32.and - (tee_local $l10 - (call $e.safe_read32s_slow_jit - (get_local $l9) - (i32.const 7))) - (i32.const 1)))) - (i32.load align=1 - (i32.xor - (i32.and - (get_local $l10) - (i32.const -4096)) - (get_local $l9))) + (i32.const 68) + (get_local $l1)) + (i32.store + (i32.const 72) + (get_local $l2)) + (i32.store + (i32.const 76) + (get_local $l3)) + (i32.store + (i32.const 80) + (get_local $l4)) + (i32.store + (i32.const 84) + (get_local $l5)) + (i32.store + (i32.const 88) + (get_local $l6)) + (i32.store + (i32.const 92) + (get_local $l7)) + (call $e.instr_F4) + (set_local $l0 + (i32.load + (i32.const 64))) + (set_local $l1 + (i32.load + (i32.const 68))) + (set_local $l2 + (i32.load + (i32.const 72))) + (set_local $l3 + (i32.load + (i32.const 76))) (set_local $l4 - (i32.add - (get_local $l4) - (i32.const 4))) - (i32.load - (i32.const 740)) - (i32.add) - (i32.store offset=556) - (br_if $L2 - (i32.ge_s - (tee_local $p0 - (call $e.jit_find_cache_entry_in_page - (i32.load - (i32.const 556)) - (i32.const 899) - (i32.const 3))) - (i32.const 0))) + (i32.load + (i32.const 80))) + (set_local $l5 + (i32.load + (i32.const 84))) + (set_local $l6 + (i32.load + (i32.const 88))) + (set_local $l7 + (i32.load + (i32.const 92))) (br $B0)) (set_local $l8 (i32.add (get_local $l8) (i32.const 1))) - (i32.store - (i32.const 560) - (i32.or + (set_local $l9 + (i32.sub + (i32.or + (i32.and + (i32.load + (i32.const 556)) + (i32.const -4096)) + (i32.const 5)) + (i32.load + (i32.const 740)))) + (set_local $l11 + (i32.add + (tee_local $l10 + (i32.sub + (get_local $l4) + (i32.const 4))) + (i32.load + (i32.const 744)))) + (block $B6 + (br_if $B6 (i32.and - (i32.load - (i32.const 556)) + (i32.eq + (i32.and + (tee_local $l12 + (i32.load offset={normalised output} + (i32.shl + (i32.shr_u + (get_local $l11) + (i32.const 12)) + (i32.const 2)))) + (i32.const 4075)) + (i32.const 1)) + (i32.le_s + (i32.and + (get_local $l11) + (i32.const 4095)) + (i32.const 4092)))) + (br_if $B1 + (i32.and + (tee_local $l12 + (call $e.safe_write32_slow_jit + (get_local $l11) + (get_local $l9) + (i32.const 0))) + (i32.const 1)))) + (i32.store align=1 + (i32.xor + (i32.and + (get_local $l12) (i32.const -4096)) - (i32.const 5))) + (get_local $l11)) + (get_local $l9)) + (set_local $l4 + (get_local $l10)) + (set_local $l8 + (i32.add + (get_local $l8) + (i32.const 2))) (i32.store - (i32.const 556) + (i32.const 120) (i32.or (i32.and (i32.load - (i32.const 556)) - (i32.const -4096)) - (i32.const 6))) + (i32.const 120)) + (i32.const -2)) + (if $I7 (result i32) + (i32.and + (tee_local $l9 + (i32.load + (i32.const 100))) + (i32.const 1)) + (then + (set_local $l9 + (i32.shr_s + (get_local $l9) + (i32.const 31))) + (i32.lt_u + (i32.xor + (i32.load + (i32.const 112)) + (get_local $l9)) + (i32.xor + (i32.load + (i32.const 104)) + (get_local $l9)))) + (else + (i32.and + (i32.load + (i32.const 120)) + (i32.const 1)))))) (i32.store - (i32.const 64) + (i32.const 104) (get_local $l0)) - (i32.store - (i32.const 68) - (get_local $l1)) - (i32.store - (i32.const 72) - (get_local $l2)) - (i32.store - (i32.const 76) - (get_local $l3)) - (i32.store - (i32.const 80) - (get_local $l4)) - (i32.store - (i32.const 84) - (get_local $l5)) - (i32.store - (i32.const 88) - (get_local $l6)) - (i32.store - (i32.const 92) - (get_local $l7)) - (call $e.instr_F4) (set_local $l0 - (i32.load - (i32.const 64))) - (set_local $l1 - (i32.load - (i32.const 68))) - (set_local $l2 - (i32.load - (i32.const 72))) - (set_local $l3 - (i32.load - (i32.const 76))) + (i32.add + (get_local $l0) + (i32.const 1))) + (i32.store + (i32.const 112) + (get_local $l0)) + (i64.store + (i32.const 96) + (i64.const 9706626088991)) + (i32.const 0) + (set_local $l9 + (i32.add + (get_local $l4) + (i32.load + (i32.const 744)))) + (block $B8 + (br_if $B8 + (i32.and + (i32.eq + (i32.and + (tee_local $l10 + (i32.load offset={normalised output} + (i32.shl + (i32.shr_u + (get_local $l9) + (i32.const 12)) + (i32.const 2)))) + (i32.const 4041)) + (i32.const 1)) + (i32.le_s + (i32.and + (get_local $l9) + (i32.const 4095)) + (i32.const 4092)))) + (br_if $B1 + (i32.and + (tee_local $l10 + (call $e.safe_read32s_slow_jit + (get_local $l9) + (i32.const 7))) + (i32.const 1)))) + (i32.load align=1 + (i32.xor + (i32.and + (get_local $l10) + (i32.const -4096)) + (get_local $l9))) (set_local $l4 - (i32.load - (i32.const 80))) - (set_local $l5 - (i32.load - (i32.const 84))) - (set_local $l6 - (i32.load - (i32.const 88))) - (set_local $l7 - (i32.load - (i32.const 92))) + (i32.add + (get_local $l4) + (i32.const 4))) + (i32.load + (i32.const 740)) + (i32.add) + (i32.store offset=556) + (br_if $L2 + (i32.ge_s + (tee_local $p0 + (call $e.jit_find_cache_entry_in_page + (i32.load + (i32.const 556)) + (i32.const 899) + (i32.const 3))) + (i32.const 0))) (br $B0)) (unreachable))) (i32.store diff --git a/tests/expect/tests/indirect-call.wast b/tests/expect/tests/indirect-call.wast index 0f48efdc73..d0ebcee8e8 100644 --- a/tests/expect/tests/indirect-call.wast +++ b/tests/expect/tests/indirect-call.wast @@ -20,11 +20,11 @@ (type $t18 (func (param i32 i64 i32))) (type $t19 (func (param i32 i64 i32) (result i32))) (type $t20 (func (param i32 i64 i64 i32) (result i32))) - (import "e" "instr_F4" (func $e.instr_F4 (type $t0))) (import "e" "trigger_gp_jit" (func $e.trigger_gp_jit (type $t2))) (import "e" "safe_read32s_slow_jit" (func $e.safe_read32s_slow_jit (type $t7))) (import "e" "safe_write32_slow_jit" (func $e.safe_write32_slow_jit (type $t16))) (import "e" "jit_find_cache_entry_in_page" (func $e.jit_find_cache_entry_in_page (type $t16))) + (import "e" "instr_F4" (func $e.instr_F4 (type $t0))) (import "e" "trigger_fault_end_jit" (func $e.trigger_fault_end_jit (type $t0))) (import "e" "m" (memory {normalised output})) (func $f (export "f") (type $t1) (param $p0 i32) @@ -68,196 +68,196 @@ (br_if $B4 (i32.eq (get_local $p0) - (i32.const 1)))) + (i32.const 0)))) (set_local $l8 (i32.add (get_local $l8) (i32.const 1))) - (i32.store - (i32.const 560) - (i32.or + (get_local $l0) + (if $I6 + (i32.load8_u + (i32.const 727)) + (then + (call $e.trigger_gp_jit + (i32.const 0) + (i32.const 0)) + (br $B1))) + (i32.load + (i32.const 748)) + (i32.add) + (set_local $l9) + (block $B7 + (br_if $B7 (i32.and - (i32.load - (i32.const 556)) - (i32.const -4096)) - (i32.const 2))) - (i32.store - (i32.const 556) - (i32.or + (i32.eq + (i32.and + (tee_local $l10 + (i32.load offset={normalised output} + (i32.shl + (i32.shr_u + (get_local $l9) + (i32.const 12)) + (i32.const 2)))) + (i32.const 4041)) + (i32.const 1)) + (i32.le_s + (i32.and + (get_local $l9) + (i32.const 4095)) + (i32.const 4092)))) + (br_if $B1 (i32.and - (i32.load - (i32.const 556)) + (tee_local $l10 + (call $e.safe_read32s_slow_jit + (get_local $l9) + (i32.const 0))) + (i32.const 1)))) + (set_local $l9 + (i32.add + (i32.load align=1 + (i32.xor + (i32.and + (get_local $l10) + (i32.const -4096)) + (get_local $l9))) + (i32.load + (i32.const 740)))) + (set_local $l10 + (i32.sub + (i32.or + (i32.and + (i32.load + (i32.const 556)) + (i32.const -4096)) + (i32.const 2)) + (i32.load + (i32.const 740)))) + (set_local $l12 + (i32.add + (tee_local $l11 + (i32.sub + (get_local $l4) + (i32.const 4))) + (i32.load + (i32.const 744)))) + (block $B8 + (br_if $B8 + (i32.and + (i32.eq + (i32.and + (tee_local $l13 + (i32.load offset={normalised output} + (i32.shl + (i32.shr_u + (get_local $l12) + (i32.const 12)) + (i32.const 2)))) + (i32.const 4075)) + (i32.const 1)) + (i32.le_s + (i32.and + (get_local $l12) + (i32.const 4095)) + (i32.const 4092)))) + (br_if $B1 + (i32.and + (tee_local $l13 + (call $e.safe_write32_slow_jit + (get_local $l12) + (get_local $l10) + (i32.const 0))) + (i32.const 1)))) + (i32.store align=1 + (i32.xor + (i32.and + (get_local $l13) (i32.const -4096)) - (i32.const 3))) - (i32.store - (i32.const 64) - (get_local $l0)) - (i32.store - (i32.const 68) - (get_local $l1)) - (i32.store - (i32.const 72) - (get_local $l2)) - (i32.store - (i32.const 76) - (get_local $l3)) - (i32.store - (i32.const 80) - (get_local $l4)) - (i32.store - (i32.const 84) - (get_local $l5)) - (i32.store - (i32.const 88) - (get_local $l6)) - (i32.store - (i32.const 92) - (get_local $l7)) - (call $e.instr_F4) - (set_local $l0 - (i32.load - (i32.const 64))) - (set_local $l1 - (i32.load - (i32.const 68))) - (set_local $l2 - (i32.load - (i32.const 72))) - (set_local $l3 - (i32.load - (i32.const 76))) + (get_local $l12)) + (get_local $l10)) (set_local $l4 - (i32.load - (i32.const 80))) - (set_local $l5 - (i32.load - (i32.const 84))) - (set_local $l6 - (i32.load - (i32.const 88))) - (set_local $l7 - (i32.load - (i32.const 92))) + (get_local $l11)) + (i32.store offset=556 + (i32.const 0) + (get_local $l9)) + (br_if $L2 + (i32.ge_s + (tee_local $p0 + (call $e.jit_find_cache_entry_in_page + (i32.load + (i32.const 556)) + (i32.const 899) + (i32.const 3))) + (i32.const 0))) (br $B0)) (set_local $l8 (i32.add (get_local $l8) (i32.const 1))) - (get_local $l0) - (if $I6 - (i32.load8_u - (i32.const 727)) - (then - (call $e.trigger_gp_jit - (i32.const 0) - (i32.const 0)) - (br $B1))) - (i32.load - (i32.const 748)) - (i32.add) - (set_local $l9) - (block $B7 - (br_if $B7 + (i32.store + (i32.const 560) + (i32.or (i32.and - (i32.eq - (i32.and - (tee_local $l10 - (i32.load offset={normalised output} - (i32.shl - (i32.shr_u - (get_local $l9) - (i32.const 12)) - (i32.const 2)))) - (i32.const 4041)) - (i32.const 1)) - (i32.le_s - (i32.and - (get_local $l9) - (i32.const 4095)) - (i32.const 4092)))) - (br_if $B1 - (i32.and - (tee_local $l10 - (call $e.safe_read32s_slow_jit - (get_local $l9) - (i32.const 0))) - (i32.const 1)))) - (set_local $l9 - (i32.add - (i32.load align=1 - (i32.xor - (i32.and - (get_local $l10) - (i32.const -4096)) - (get_local $l9))) - (i32.load - (i32.const 740)))) - (set_local $l10 - (i32.sub - (i32.or - (i32.and - (i32.load - (i32.const 556)) - (i32.const -4096)) - (i32.const 2)) - (i32.load - (i32.const 740)))) - (set_local $l12 - (i32.add - (tee_local $l11 - (i32.sub - (get_local $l4) - (i32.const 4))) - (i32.load - (i32.const 744)))) - (block $B8 - (br_if $B8 - (i32.and - (i32.eq - (i32.and - (tee_local $l13 - (i32.load offset={normalised output} - (i32.shl - (i32.shr_u - (get_local $l12) - (i32.const 12)) - (i32.const 2)))) - (i32.const 4075)) - (i32.const 1)) - (i32.le_s - (i32.and - (get_local $l12) - (i32.const 4095)) - (i32.const 4092)))) - (br_if $B1 - (i32.and - (tee_local $l13 - (call $e.safe_write32_slow_jit - (get_local $l12) - (get_local $l10) - (i32.const 0))) - (i32.const 1)))) - (i32.store align=1 - (i32.xor + (i32.load + (i32.const 556)) + (i32.const -4096)) + (i32.const 2))) + (i32.store + (i32.const 556) + (i32.or (i32.and - (get_local $l13) + (i32.load + (i32.const 556)) (i32.const -4096)) - (get_local $l12)) - (get_local $l10)) + (i32.const 3))) + (i32.store + (i32.const 64) + (get_local $l0)) + (i32.store + (i32.const 68) + (get_local $l1)) + (i32.store + (i32.const 72) + (get_local $l2)) + (i32.store + (i32.const 76) + (get_local $l3)) + (i32.store + (i32.const 80) + (get_local $l4)) + (i32.store + (i32.const 84) + (get_local $l5)) + (i32.store + (i32.const 88) + (get_local $l6)) + (i32.store + (i32.const 92) + (get_local $l7)) + (call $e.instr_F4) + (set_local $l0 + (i32.load + (i32.const 64))) + (set_local $l1 + (i32.load + (i32.const 68))) + (set_local $l2 + (i32.load + (i32.const 72))) + (set_local $l3 + (i32.load + (i32.const 76))) (set_local $l4 - (get_local $l11)) - (i32.store offset=556 - (i32.const 0) - (get_local $l9)) - (br_if $L2 - (i32.ge_s - (tee_local $p0 - (call $e.jit_find_cache_entry_in_page - (i32.load - (i32.const 556)) - (i32.const 899) - (i32.const 3))) - (i32.const 0))) + (i32.load + (i32.const 80))) + (set_local $l5 + (i32.load + (i32.const 84))) + (set_local $l6 + (i32.load + (i32.const 88))) + (set_local $l7 + (i32.load + (i32.const 92))) (br $B0)) (unreachable))) (i32.store diff --git a/tests/expect/tests/sti.wast b/tests/expect/tests/sti.wast index 47f284d150..59a4177721 100644 --- a/tests/expect/tests/sti.wast +++ b/tests/expect/tests/sti.wast @@ -67,7 +67,7 @@ (br_if $B4 (i32.eq (get_local $p0) - (i32.const 1)))) + (i32.const 0)))) (set_local $l8 (i32.add (get_local $l8) diff --git a/tests/full/run.js b/tests/full/run.js index 744cadc827..4685166eb9 100755 --- a/tests/full/run.js +++ b/tests/full/run.js @@ -1146,7 +1146,6 @@ function run_test(test, done) clearInterval(screen_interval); } - emulator.stop(); emulator.destroy(); if(test.failure_allowed) diff --git a/tests/jit-paging/run.js b/tests/jit-paging/run.js index 735e09d5e5..4bdc903cbf 100755 --- a/tests/jit-paging/run.js +++ b/tests/jit-paging/run.js @@ -63,7 +63,7 @@ emulator.add_listener("serial0-output-byte", async function(byte) const data = await emulator.read_file("/result"); - emulator.stop(); + emulator.destroy(); let result = Buffer.from(data).toString(); if(result !== "test_shared passed\ntest_consecutive_written passed\n") diff --git a/tests/manual/gc.html b/tests/manual/gc.html index 2c9651aedf..68053de9dd 100644 --- a/tests/manual/gc.html +++ b/tests/manual/gc.html @@ -19,7 +19,6 @@ }); setTimeout(() => { - emulator.stop(); emulator.destroy(); console.log("Emulator freed. Check using devtools (in chromium: Memory -> Heap Snapshot -> click collect garbage -> take snapshot)."); }, 3 * 1000); diff --git a/tests/qemu/run.js b/tests/qemu/run.js index 51c593fc03..b4b0d7d2a9 100755 --- a/tests/qemu/run.js +++ b/tests/qemu/run.js @@ -64,6 +64,6 @@ emulator.add_listener("serial0-output-byte", async function(byte) console.error("Got result, writing to stdout"); process.stdout.write(Buffer.from(data)); - emulator.stop(); + emulator.destroy(); } }); diff --git a/tools/docker/alpine/Dockerfile b/tools/docker/alpine/Dockerfile index 0e7354b0c2..62b4c6dfde 100644 --- a/tools/docker/alpine/Dockerfile +++ b/tools/docker/alpine/Dockerfile @@ -1,17 +1,9 @@ -FROM docker.io/i386/alpine:3.20.0 +FROM docker.io/i386/alpine:3.21.0 -ENV KERNEL=lts +ENV KERNEL=virt ENV ADDPKGS=nodejs -RUN apk add openrc alpine-base agetty alpine-conf $ADDPKGS - -RUN if [ "$KERNEL" == "lts" ]; then \ - apk add linux-lts \ - linux-firmware-none \ - linux-firmware-sb16; \ -else \ - apk add linux-$KERNEL; \ -fi +RUN apk add openrc alpine-base agetty alpine-conf linux-$KERNEL linux-firmware-none $ADDPKGS RUN sed -i 's/getty 38400 tty1/agetty --autologin root tty1 linux/' /etc/inittab RUN echo 'ttyS0::respawn:/sbin/agetty --autologin root -s ttyS0 115200 vt100' >> /etc/inittab @@ -19,11 +11,8 @@ RUN echo "root:" | chpasswd RUN setup-hostname localhost -# Adding networking.sh script (works only on lts kernel yet) -RUN if [ "$KERNEL" == "lts" ]; then \ - echo -e "rmmod ne2k-pci && modprobe ne2k-pci\nhwclock -s\nsetup-interfaces -a -r" > /root/networking.sh && \ - chmod +x /root/networking.sh; \ -fi +# Adding networking.sh script +RUN echo -e "rmmod ne2k-pci && modprobe ne2k-pci\nrmmod virtio-net && modprobe virtio-net\nhwclock -s\nsetup-interfaces -a -r" > /root/networking.sh && chmod +x /root/networking.sh RUN echo 'console.log("Hello, world!");' > /root/hello.js @@ -33,4 +22,4 @@ RUN for i in hwclock modules sysctl hostname syslog bootmisc; do rc-update add $ RUN rc-update add killprocs shutdown # Generate initramfs with 9p modules -RUN mkinitfs -F "ata base ide scsi virtio ext4 9p" $(cat /usr/share/kernel/$KERNEL/kernel.release) +RUN mkinitfs -F "base virtio 9p" $(cat /usr/share/kernel/$KERNEL/kernel.release) diff --git a/tools/docker/alpine/Readme.md b/tools/docker/alpine/Readme.md index 1256f0481d..ac2dabbb1d 100644 --- a/tools/docker/alpine/Readme.md +++ b/tools/docker/alpine/Readme.md @@ -1,6 +1,6 @@ You can build a Alpine Linux 9p image using Docker: -1. As needed, kernel flavor (`virt` is smaller than `lts`, but don't have networking) and set of additional packages (community repo is enabled by default) can be edited in `Dockerfile` +1. As needed, kernel flavor (`virt` is smaller than `lts`) and set of additional packages (community repo is enabled by default) can be edited in `Dockerfile` 2. Check and run `./build.sh` with started dockerd (podman works) 3. Run local webserver (e.g. `make run`) and open `examples/alpine.html` 4. (optional) Run `./build-state.js` and add `initial_state: { url: "../images/alpine-state.bin" }` to `alpine.html` diff --git a/tools/docker/alpine/build-state.js b/tools/docker/alpine/build-state.js index 2a2f3341f8..fd866b9bfd 100755 --- a/tools/docker/alpine/build-state.js +++ b/tools/docker/alpine/build-state.js @@ -51,7 +51,7 @@ emulator.add_listener("serial0-output-byte", function(byte) { if(e) throw e; console.log("Saved as " + OUTPUT_FILE); - emulator.stop(); + emulator.destroy(); }); }, 10 * 1000); } diff --git a/v86.css b/v86.css index d25f480c18..5088ef52e3 100644 --- a/v86.css +++ b/v86.css @@ -63,6 +63,7 @@ body { a { color: wheat; text-decoration: none; + cursor: pointer; } .phone_keyboard { width: 0; @@ -106,27 +107,80 @@ h4 { font-weight: bold; font-size: 16px; } -#boot_options td { +#boot_options td, #boot_options th { padding: 1px 7px; } -#oses small { - font-size: 80%; - color: #ccc; - padding-left: 5px; -} -#oses tr { + +#oses tbody tr { cursor: pointer; } #oses { border-spacing: 0; } -#oses tr:hover { +#oses tbody tr:hover { background-color: #311; } -#oses td:nth-child(1) { - white-space: pre; - vertical-align: top; +#oses td:nth-child(2) { + text-align: right; +} +#oses thead { + text-align: left; +} + +/* This is the best I managed to do with my little css experience. + If you can do better, please send a PR. */ +@media (max-width: 1250px) { + #oses td:nth-child(9), #oses th:nth-child(9) { + display: none; + } } +@media (max-width: 1150px) { + #oses td:nth-child(8), #oses td:nth-child(7), #oses th:nth-child(8), #oses th:nth-child(7) { + display: none; + } +} +@media (max-width: 1050px) { + #oses td:nth-child(5), #oses td:nth-child(6), #oses th:nth-child(5), #oses th:nth-child(6) { + display: none; + } +} +@media (max-width: 850px) { + #oses td:nth-child(4), #oses th:nth-child(4) { + display: none; + } +} +@media (max-width: 750px) { + #oses th:nth-child(2), #oses th:nth-child(3), #oses th:nth-child(10) { + display: none; + } + #oses td:nth-child(1), #oses td:nth-child(2), #oses td:nth-child(3) { + display: inline; + } + #oses td:nth-child(2), td:nth-child(3) { + font-size: smaller; + } + #oses td:nth-child(10) { + display: block; + padding-bottom: 10px; + } +} + +label { + user-select: none; + white-space: nowrap; +} +input[type=checkbox] { + vertical-align: middle; + position: relative; + bottom: 1px; + margin: 0; +} +#filter label { + background-color: #444; + padding: 2px 4px; + border-radius: 4px; +} + #terminal { max-width: 1024px; } @@ -147,6 +201,15 @@ h4 { } } +.gui_icon { + height: 1em; + content: url('data:image/svg+xml,'); +} +.tui_icon { + height: 1em; + content: url('data:image/svg+xml,'); +} + /* the code below was copied from xterm.css */ .xterm {

+ Presets: none, public relay, wisp, fetch