feat: 3d acceleration, bunch of reworks - #52
Open
HuJK wants to merge 39 commits into
Open
Conversation
HuJK
force-pushed
the
pr/3d-accel
branch
3 times, most recently
from
September 1, 2026 12:19
85c6ba2 to
bc52566
Compare
LICENSE.txt has always been the full GPLv3, but not one source file said so: 397 Java files with no header at all, so the licence of a file could only be inferred from the repository around it. Every source file now carries an SPDX-License-Identifier line. ADDITIONAL-PERMISSIONS is the part that is new rather than merely written down. It lets anyone relicense this material under the terms an upstream project requires, for the purpose of getting it merged there and only for that -- which is what these changes exist for, and what a plain GPLv3 file cannot do when the project it is aimed at is Apache-2.0 or BSD. Contributions carry the permission because they are made under the project licence; CONTRIBUTING.md asks for a DCO sign-off rather than a CLA, so nobody signs away anything to hold it. LICENSE.txt itself is left alone. It is reflowed rather than hard-wrapped the way the FSF renders it, which is a cosmetic difference in a file that should not be churned.
…logs check_arity.py walls off the constructors and methods that had grown past the point where a caller can be read: a positional list long enough that swapping two arguments of the same type still compiles. check_vector_icons.py holds the drawables to one shape so an icon added in a hurry does not arrive at a different optical size than the row it sits in. check_string_concat gets stricter, and the two allow-lists carry the deliberate exceptions rather than leaving the checks off. isReturnDefaultValues lets a unit test cover a class that logs. Keeping every testable class free of android.util.Log stopped being tenable once the classes worth testing were the ones whose whole subject is a socket and a thread outliving the object that owned them, and which say so out loud when they do. Nothing asserts on a stub's return value; the stubs only let the class under test be built.
The two display screens had grown the same features twice and disagreed about half of them. A DisplaySource interface now names what a screen consumes (a Surface for the native path, a bitmap for VNC), and the viewport and chrome controllers hold the parts that were duplicated: the fit/zoom/pan arithmetic, the toolbar and its auto-hide, the resolution-follow that resizes the guest to the window it is being shown in. Input is routed once. Pointer motion is coalesced before it reaches the guest, gestures are translated in one place with an explicit mode (touch, mouse, tablet) instead of each screen guessing, and the keyboard grew what a keyboard needs: real key holds rather than tap-to-toggle, a laptop layout, a physical keyboard view, and an extra-keys row that fits on a phone. Scan codes are generated from one table -- the letters used to be wrong, so typing produced different characters than the key that was pressed. Two failure modes are fixed here because they belong to the framework rather than to either screen. The binder wait for the display root service could leak threads and retried without a cap, so a VM that had already stopped left the app spinning; it now waits once and gives up with the VM. And a crosvm that restarts under a live display used to leave the surfaces pointing at a dead process -- the reconnect rebuilds them. Co-authored-by: lateautumn233 <lateautumn233@foxmail.com>
HuJK
force-pushed
the
pr/3d-accel
branch
5 times, most recently
from
September 2, 2026 23:21
6d5d235 to
7ceb2f3
Compare
Importing an LXC rootfs and turning it into a bootable VM used to be a sequence of steps a user had to know: import, create a disk, size it, write a config, hope the compression was one crosvm can boot. It is one flow now, and the pieces it needs are here -- the image picker, the create-Linux-VM screen, the command generation, and the deletion path that takes the disks with it. Overlays become a tree rather than a list. A qcow2 with a backing file is a node with a parent, and the tree is what makes branch, switch and collapse expressible at all; the backing chain linker and the dependency updater are what keep the chain honest when a node moves. Disk maintenance -- resize, recompression, auto-grow -- hangs off the same tree. The rules that used to bite late now bite early. crosvm refuses to boot a compressed qcow2 cluster it cannot decompress, so there is one definition of what "compressed" means and imports stop re-compressing what is already right; it refuses a disk with a snapshot, and the editor says so before the VM is started rather than after; and a disk that has not finished importing cannot be selected as a boot device. Co-authored-by: lateautumn233 <lateautumn233@foxmail.com>
…, boot plans Four small things that share nothing except the VM they configure. The guest is told the real host CPU. crosvm forwards --smbios processor-version through FDT /chosen and EDK2 publishes it as SMBIOS Type 4, so a UEFI guest shows "Qualcomm Snapdragon 8 Elite" rather than the firmware default "Gunyah vCPU". Game mode is declared so the platform raises CPU and GPU clocks for a running VM instead of treating it as a background app. Per-VM environment variables are read out of the config and applied to the crosvm process. BootPlan stops being one constructor with nine positional arguments where four of them are always empty. There are two kinds of plan -- UEFI firmware plus vars, or a kernel plus initrd plus cmdline -- and each now has a constructor that takes only its own fields, so a plan cannot be built half in one mode and half in the other. The swiotlb size travels with the plan for the same reason: it is a property of how the VM boots, not of the display or the GPU. Co-authored-by: lateautumn233 <lateautumn233@foxmail.com>
…what it drives The modules this app ships have to be matched to the kernel that is running before they can be loaded, and the matching is not a substring test: KMI directories are picked by exact version token, because 6.1 matching 6.12 loads a module built for the wrong ABI and the failure arrives later, as a kernel oops. match.json holds the rules and SocIdentity supplies the SoC half. Autostart is only armed for a module that has actually been seen loaded, so a module that fails once does not fail on every boot from then on. Each module gets a description page, shipped with the module rather than written into the app, so the answer to "what is this and why would I load it" is in one place. The huge-page panel is a front end for gh-hugepage-reserve, an external Magisk module, and that is worth saying plainly: it reads and writes that project's settings.prop and its sysfs, branches on its v6/v7 differences, and links to its releases page. When the module is absent the whole panel is off -- nothing here fabricates a reserve. The advanced screen exposes the five knobs the GUI cannot decide on the user's behalf, and an empty field means the module's own default rather than zero. PoolPreflight is the reason the panel matters to a VM that is starting. Starting into the reserve's refill gap makes the hypervisor take movable memory and migrate it out of CMA instead, which has been seen as a multi-minute whole-host stall ending in the kernel OOM-killing crosvm, and as "qcom_scm: Assign memory protection call failed -22", which resets the device. The preflight sizes a VM against the free pool -- guest RAM, swiotlb, and the route's GPU pools -- in the module's own unit of 2 MB pages, and refuses before any of that can happen.
… VM exits Symptom: after a few VM runs the app will not start any more and only a reboot fixes it. Cause: setuid()'s set_user() -- which updates cred->user, the field commit_creds() looks at to decide whether to move the NPROC charge -- sits behind the CAP_SETUID branch. On the saved-uid path only cred->ucounts is updated, so the process's NPROC charge is never added but is still subtracted when it exits. Once the counter goes negative, is_rlimit_overlimit() fails every fork for that uid and Zygote can no longer start anything. The fix is setresuid() where the switching happens. This is the rescue for the machines that have already drifted: nproc_guard takes the uid as an insmod parameter, so the module is loaded with this app's uid, and the VM lifecycle nudges a reset once a VM has exited. There is no hard dependency in either direction -- with the module absent its sysfs node does not exist and this side is a no-op.
A VM with no network is not much of a VM, and the network editor asks for a bridge type, an uplink mode, a subnet and a VLAN before it will produce one. NetworkPresets answers those questions for the two cases that cover almost everybody: a Wi-Fi pseudo-bridge, where the VM sits on the phone's own segment and takes its address from the upstream router (Wi-Fi in station mode cannot be enslaved into a Linux bridge, hence pseudo), and a routed network with one untagged VLAN, NAT to whatever uplink the host has, and DHCP. The editor builds its blank form from the same helpers, so a network created in one tap and one typed out by hand pick their subnets from the same pool and avoid the same conflicts. The setup wizard gains a step that makes one, because first run is exactly when the user does not yet have an opinion about bridge types.
… panel Some things can only be done from inside the guest -- changing a root password whose value nobody remembers, growing a filesystem to match a disk that was just resized. The agent runs the guest headless with an extra agent0 chardev on the QEMU backend, drives it through a queue of actions with a declared spec each, and shows what it is doing. The queue is ordinary enough to unit-test, and is. The password is typed rather than embedded. Building it into the script that runs inside the guest puts it in a command line, a log and a config file; it is sent over the agent channel instead, and the payload is chunked because a console is not a pipe and a long line does not survive one. The console, the agent screen and the disk operation screen were three copies of the same terminal, and only one of them could be typed into. They share TerminalPanelView now. The console and the disk screen are the refactor's passengers; the agent screen is the change -- a rescue shell you can see is worth much less than one you can answer.
…in the editor Scheduling a VM's vCPUs onto a big.LITTLE phone by hand is the difference between a desktop that responds and one that does not, and crosvm already takes --cpu-affinity, --cpu-capacity and --cpu-cluster for it. What was missing was a way to say what you wanted. CpuPlacementPlan holds the answer -- per-vCPU core sets, capacities and clusters -- validates it, and turns it into those three arguments; the editor picks it with a core picker rather than a text field that accepts anything. The GPU worker cpuset comes with it, because it is the same kind of setting made of the same host cores, and because what depends on it comes later: the cgroup path is created before crosvm starts, since crosvm opens <cgroup>/tasks itself and a path that appears afterwards is a path it never saw. Co-authored-by: lateautumn233 <lateautumn233@foxmail.com>
…hind them A VM had one console and no way to ask for another. Serial ports are now a peripheral category of their own: each port names its hardware (SBSA UART or virtio-console) and its backend (stdio, a pty, a file, or a USB ACM function), and the crosvm arguments are generated from that rather than from a special case for the console. The USB ACM backend is the one with a host side. The gadget's ACM functions are a shared pool -- there is one USB device on the phone and every VM that wants a port takes a slot out of it -- so UsbAcmPool reconciles the configfs gadget to the number of ports configured, and keeps track of who holds which slot. Unbinding the gadget needs a newline written to UDC and a moment for USB to settle; without the wait the rebind lands on a controller that has not finished tearing down and the whole gadget stops enumerating. The VM info screen stops calling a stdio port a "text console" and names it after the backend it actually is, because with a list of ports that is now an answerable question.
…a purpose The card is one device with a list of endpoints rather than one anonymous stream. Each endpoint carries a direction, a purpose, a buffer size and an underrun policy, and names a host device to open. Endpoints are grouped by direction in the editor so a playback and a capture endpoint are not two rows that look identical. The host devices are enumerated and published rather than guessed. AAudio reports device ids that are reassigned on every boot, so an endpoint stores a type and an address and resolves that to an id at start time; a device that cannot be opened is not offered at all, which is a better answer than an endpoint that fails when the guest first touches it. Recording needs RECORD_AUDIO, and the permission is asked for where the endpoint is configured rather than at the point where the failure would be silent.
The configuration half of virtio-media: a camera peripheral and a VPU peripheral in the editor, their schema, the host camera enumeration behind the picker, and the CAMERA permission. A foreground service holds the peripheral open, because a camera that stops delivering frames the moment the app leaves the foreground is not a camera a guest can use. The devices themselves are not written yet. This is deliberately the half that can land first -- it is what the crosvm side will be configured by -- but it means the rows here describe hardware that does not answer yet.
A protected VM has its memory lent to the guest, so every virtio buffer has to travel through a bounce pool and the guest kernel needs CONFIG_RESTRICTED_DMA_POOL to build one. No distribution ships that, which is why every stock image failed to boot in the only mode that Gunyah's protected path offered. PSEUDO_UNPROTECTED is protected to the hypervisor, but the guest's window is SHARE'd back before the payload runs, so the host can still reach it and there is nothing to bounce through. A stock distribution kernel boots. Two consequences are wired here rather than left to the user: the mode is not in the list the boot tab warns about, because the warning would be false; and the swiotlb size is ignored in this mode and hidden in the editor, because a restricted-dma-pool node in the device tree of a guest that was never built to honour one is the exact thing this mode exists to avoid.
… lending fault names the device Dynamic memory sharing is not a checkbox a user should have to find. Features that need it -- the GPU pools, the host-visible blob paths -- declare it, and the editor turns it on for them and moves it to the basic tab where a VM-shaped setting belongs. Under Gunyah the RingBlob backing is pinned (gunyah-pvm) so the permanent SHARE that maps it stays valid. The tab strip changes because the sections moved. It spreads across the full width when the tabs fit and scrolls when they do not, and it leaves its drags to the tab strip rather than fighting the pager underneath. The rest is the diagnostic, and it exists because the failure it explains is unreadable without it. When lending fails the kernel reports a hypervisor error and a config symbol, and neither says which device asked for the memory. The handler now names the devices behind the fault and links to something a reader can act on, and the console log can be filtered so the line is findable in a boot's worth of output.
Host-visible memory that gfxstream hands the guest has to be reachable from both sides, and on a protected VM that means a Gunyah SHARE. Doing it per allocation puts a hypervisor call on the allocation path, and on the fragile sm8650 RM a stream of them is what precedes a SoC reset. --pre-alloc gfx-host-mb= claims the arena once, before the guest is started, and every host-visible allocation is sub-allocated out of it with no runtime SHARE at all. The size is a field in the graphics tab rather than a constant, because how much a guest needs is a property of what it runs.
… see the RAM The other half of the same problem, from the other end. Instead of the host owning a pool and lending it to the guest, the guest owns one and hands the host dma-bufs out of it -- udmabuf=true is what gates VIRTIO_GPU_F_CREATE_GUEST_HANDLE, and gpu-guest-mb declares the region. The guest driver keeps one allocator, so the same pool serves whichever renderer is configured. It is only worth anything where the host cannot otherwise reach the guest's memory. In an unprotected VM, or a pseudo-unprotected one whose window is shared back before the payload runs, the pool is memory taken from the guest to solve a problem that is not happening -- and virtio-gpu with no pool node to find allocates from system RAM instead, which the host can read for the same reason. The editor hides the field in those modes, and the backend zeroes the stored size rather than trusting it, because a config that arrives through the daemon API or that was saved before the mode was switched still carries one. Co-authored-by: lateautumn233 <lateautumn233@foxmail.com>
Declaring the guest pool at the size of the worst case and SHARE'ing all of it at boot costs that memory for the whole life of the VM, most of it never touched. The pool is declared at full size but only the preallocation is SHARE'd before boot; the guest grows and shrinks the rest at run time, one memparcel per step, up to a ceiling on how many grants may be outstanding. The three knobs are in the editor because none of them has a right answer: a step too small makes a stall out of every allocation burst, a step too large gives back the saving, and the grant ceiling is what keeps a runaway guest from filling the hypervisor's table. Their defaults are the old behaviour -- preallocation equal to the pool, step zero, no grants -- so a config that says nothing gets a pool that does not grow. Co-authored-by: lateautumn233 <lateautumn233@foxmail.com>
udmabuf defaults to a 64 MB cap per handle, and a whole host-visible allocation is routinely larger than that, so the import fails and the blob path falls back to something slower or not at all. The limit is raised at VM start. The glob matters: kernels without CONFIG_UDMABUF get the fallback module this app ships, which lands under a versioned name (/sys/module/udmabuf_gki_6.1/...), so writing only to /sys/module/udmabuf would raise the limit on exactly the kernels that did not need the fallback.
Getting a frame from the VM to a Surface is either a memcpy or a dma-buf import plus a GPU blit, and which one happens depends on a driver being loadable and having the extensions for a raw dma-buf import. That is a separate axis from the renderer: the same import path serves the virtio-gpu scanout and the simplefb framebuffer, and it is used by the native bridge blitting into a Surface and by the VNC sink blitting into a headless target of its own. So the blit provider is its own setting -- the bundled turnip, the SoC's stock Vulkan HAL, or off -- and vkprobe answers whether the choice will work before it is offered, by opening the driver and looking for the extensions rather than by hardcoding a vendor list. Qualcomm's stock driver lacks raw dma-buf import, so SYSTEM degrades to the CPU copy instead of half-loading a driver that cannot do the job. An unwired provider is listed and refused with a note rather than silently treated as one of the others, because a silent downgrade is the failure that looks like success. simplefb gets the configured geometry rather than borrowing the GPU screen's, which is what makes it a device the blit path can treat like any other source.
The graphics tab described one display with a GPU bolted to the side of it: one width, one height, one refresh rate for whichever screen you happened to be looking at, and a VM-wide input path underneath. Two display devices with one geometry between them is one number answering two questions -- a 1400x1050 virtio-gpu mode and a 1280x720 framebuffer are an ordinary pair and the schema could not say it. A screen is now an object with its own size, its own rate, and the exporter that shows it. The virtio-gpu screen takes a refresh rate because that is a field of a mode the guest is told about; simplefb takes a poll rate instead, because nothing in that device announces a frame -- the guest maps the region write-combining and no write traps -- so the rate the host samples at is the only thing that decides when a picture exists. Size and rate are picked from menus of what the device can actually do rather than typed. Input follows the scanout rather than the VM. A tablet and a keyboard belong to the screen being exported, so two screens get two of each and the one you are touching is the one that receives; the VM-wide input set retires with the arbitration it belonged to. The transport between a screen and its exporter is negotiated as a ceiling, not a demand: the source says what it can produce, the sink what it can consume, the highest rung both reach wins, and a user's choice only restricts downward -- so every value is honourable and nothing silently downgrades. Rungs that are merely unbuilt are listed and refused with a note so the ladder reads whole.
The console's VNC client advertises the Open H.264 encoding (50) and a private DroidVM pseudo-encoding (DVH1), and reads the hardware stream off the one RFB connection it already has for pixels and input. The separate side-channel port is gone -- listener, config key, editor field, daemon argument and vnc_info field. A third-party client that speaks encoding 50 gets the same stream; one that does not still gets pixels. Both rects are parsed in one place: encoding 50 as u32 length + u32 flags + Annex-B honouring the reset flags, and the pseudo-encoding as a capabilities and heartbeat payload. Those two are what RFB negotiation cannot express and the reason the app speaks more than plain encoding 50 -- capabilities tell a permanent "no encoder" apart from one that is still warming, and the heartbeat tells a still screen apart from a dead stream. Cold open is the case that decides the design. crosvm sends SPS/PPS once at join and never again on a later bare IDR, but a TextureView's SurfaceTexture is created a frame after the view is shown, so the first parameter-set-bearing rect can arrive before any decoder exists to feed. The pipeline caches the reset-flagged sync rect per coded geometry and replays it to a decoder that attaches afterwards, which is what makes cold open, reconnect and a guest resize all reach the decoder with parameter sets. The outgoing decoder is released on resize; it used to leak its MediaCodec.
crosvm can promote the virtio-gpu worker to SCHED_FIFO. On gfxstream its per-context render threads inherit that policy and spin-wait on the guest command ring, which lets them starve the normal-priority vCPU that feeds them -- a priority inversion that caps the present rate rather than raising it. So real-time is opt-in and off by default. It is also refused without the cpuset, rather than silently applied. RT confined to the picked cores trades vCPU latency for render throughput on those cores, which is the point of the switch; RT with no cpuset is a different thing entirely -- FIFO 97 threads eligible for every core on the phone, above everything else Android is running. The editor cannot save that combination and the backend logs and skips it, because a config built through the daemon API, or one whose cpuset setup soft-failed, can still arrive carrying it.
…kan capset The guest runs its own turnip over vdrm and virglrenderer translates the msm protocol into KGSL ioctls, so nothing is remoted at the GL or VK level and the host advertises no Vulkan capset at all. Only the DRM capset goes out: not advertising VIRGL2 is what keeps a stock guest on llvmpipe, which displays correctly, instead of on host-GL virgl, whose buffers the CPU scanout cannot read back -- black until the guest additions are installed. The route needs two pools and they hold different things. drm-host-mb is the host arena, now only the per-context msm shmem rings, so single-digit MB rather than the gigabyte the buffer objects used to need; the guest's drm_buddy pool, where every BO actually comes from, is the shared guest pool. That pool needs udmabuf=true, which is what gates VIRTIO_GPU_F_CREATE_GUEST_HANDLE -- without it guest mesa silently keeps a host-allocating path this host no longer implements, the VM boots, the desktop comes up, and the failure waits for the first large allocation. The editor's GPU section splits into what is proxied and who serves it, because "renderer" was answering both and the answers are not the same axis. A config written before the split carries only gpu_api, so gpu_mode falls back to the same migration the editor shows, and the ANGLE option is gone: crosvm has no angle key and rejects unknown ones, so selecting it stopped the VM from starting at all.
gfxstream serves both VK (turnip) and GL-on-VK (zink) clients from one host process, so the guest gets the gfxstream-vulkan context type with both capsets on. pci-bar-size is the host-visible BAR window and doubles as the GPU memory ceiling, because the guest has no device-local-only memory type to bound it some other way. Dynamic VRAM is the host-alloc branch, and vram-limit is what turns it on: a defined, non-zero limit supplies gfxstream's folio quota, the VK_EXT_memory_budget capacity handed to the guest driver, and -- by being defined at all -- enables fusion routing. Leaving it undefined keeps every allocation inside the pre-allocated pool. It is only sent for host-alloc, because under a guest-owned pool the pool is already the cap and crosvm ignores it. The folio threshold and the fusion size gate are the two knobs that decide which allocations are worth routing which way. The host ICD is chosen here too. gfxstream dlopens ANDROID_EMU_VK_LOADER_PATH ahead of the system loader, so the bundled turnip is pointed at unless the provider row asks for the SoC's stock HAL, and it falls back to the system HAL if the turnip file is missing.
…t pool Venus is Vulkan proxied through virglrenderer: capset venus, guest-allocated blobs, and the same host ICD and udmabuf import path gfxstream uses, which is why it shares that env rather than getting one of its own. vulkan=true maps to use_venus on the virgl path, and the capset forces use_venus and use_guest_vram host-side, so the two together are belt and braces. venus-host-mb is the pool the command-stream transport lives in: the per-instance ring and the CS and reply chunks, every blob_id==0 shmem sub-allocated out of one pre-shared region that the guest maps at pool_base plus offset with no runtime SHARE. Sizing it too small is not a slowdown, it is a hazard -- each blob that does not fit falls back to a per-blob memfd, which means a runtime SHARE, and a stream of those SoC-resets the sm8650 RM. The default is sized for the KDE-plus-vkmark transport peak, where the CS pool alone is 8 MB or more per instance. The provider row for this route names the host Vulkan ICD rather than "Venus", because Venus is the route and the ICD is the choice.
A customer's a14 device cannot link crosvm: libgfxstream_backend.so
(built against the a16 soong libc++) imports
_ZNKSt3__14__fs10filesystem4path10__filenameEv and a14's platform
libc++ predates the out-of-line path parsers. A scan of every payload
binary shows it is the only __fs::filesystem import, so one jump alias
into the c++_static __ndk1 implementation covers it -- same pattern as
the filebuf shims next to it; path is {basic_string} and the return is
a string_view in both namespaces.
The paired report, CANNOT LINK against our own libnativewindow.so
(GraphicBuffer::getDataspace missing from a14's libui), needs no app
change: the meta repo stopped shipping the platform-shadow libraries
and cleanupPrebuilt() already deletes them on upgrade.
… by kind A package carries one network per distinct one its VM's adapters were on, and they need not be alike -- an L2 bridge and a routed gVisor network travel together fine -- so the single whole-package choice could not say anything useful about either. Each packaged network now gets its own card: join one this phone already has, create it here, or leave it behind, in that order, with the default landing on the first that is actually available. Joining is what carries the settings that only exist for one kind of network: an L3 VLAN's DHCP pool offsets, a gVisor network's IPv6 SNAT. So the target list is exactly the networks of the same bridge type and uplink mode, closest first -- by IPv4 prefix (IPv6 when the packaged network has no IPv4), or by uplink name for L2 -- which puts the network the user means at the top. An empty list greys "join" out and says so. The other two greyings are the same question asked the other way. Creating is refused when the packaged network would collide with one already here, and that check is now scoped: two Linux bridges route in the host kernel and cannot hold the same prefix, two gVisor networks collide the same way inside their own user-space stacks, but a Linux bridge and a gVisor network never see each other's routes at all -- gVisor's addressing lives in its process, the kernel has no idea the prefix exists -- so the same subnet on both is fine, and refusing it only cost address space. An L2 network has no prefix to collide with; what it cannot share is the physical uplink it bridges, which one network at a time owns (a rule the editor now enforces too). Names are deliberately not scoped: the display name and the bridge name stay unique app-wide, which is why they are also the one thing an import may quietly change, and the card shows what it settled on before the import runs. At most one of "join" and "create" is ever greyed, because a collision proves a network of that kind exists, and that network is a join candidate. Static DHCP leases come across too. An offset from the other phone is kept where it can be -- it is the address the guest has been answering on -- and otherwise moves to the next free one, wrapping rather than stopping at the top of the VLAN, since the fallback is a dynamic address. Only when the VLAN has nothing free, does not serve that family, or the NIC ended up on no network at all does the lease go back to dynamic. A kept lease also gets a MAC: exporting strips them (two phones must not hand out the same one) and a static lease is keyed by MAC, so waiting for the user to open the NIC editor would leave the VM unable to start. The screen and the import task share NetworkImportPlan, so what the user is shown is what gets built; the request carries the decisions as network_plan, with network_mode kept as the fallback for a ref the plan does not mention. Verified on device: cross-type same-subnet accepted, same-type refused, L2 uplink refused, join reuses the network and moves a colliding offset 70 -> 71, create renames to lan50_1/brlan501 and gv50_1/brgv501 with the bridge type intact. Signed-off-by: HuJK <gh@hujk.org>
Deleting a network refused with "in use by running VMs" and named every VM
attached to it, running or not, so a network any VM had ever been attached to
could not be deleted at all.
The check reads vm_list, which reports the state as the enum name ("STOPPED"),
and compared it against lowercase "stopped" -- vm_status is the one that
lower-cases, and every other state check in the UI is already case-insensitive.
Found while testing vmpkg import on device: three stopped VMs held gv50 and the
delete silently did nothing.
Signed-off-by: HuJK <gh@hujk.org>
… when it starts
The daemon reads networks.json once, at startup, and otherwise learns about a
network only through network_create / network_modify. The editor sent neither
for a network that did not exist there yet -- it asked network_exists and, when
told no, sent nothing -- so a network created in the app and never started was
invisible to the daemon until the next restart.
Two things broke on that gap, both silently:
* starting a VM whose NIC named such a network failed in setupTap with
"Network <id> not found";
* exporting that VM packed no network at all -- collectNetworks looks the id
up in the daemon's store, found nothing, and skipped it, so the package's
NICs referenced networks it did not carry and the import had nothing to
offer.
So both writers of networks.json now sync: the editor for create as well as
modify, and the setup wizard, which normally runs before there is a daemon but
not always. Registering is not starting -- the instance lands STOPPED, exactly
as it would have on the next daemon start.
The export keeps its skip as a last resort, but says so in the log now: a
package quietly missing a network is not something anything else would report.
Signed-off-by: HuJK <gh@hujk.org>
The recorded commit was one a local build produced on a branch nobody publishes, so a fresh clone resolved it to nothing. This one is what the CI packaged from DroidVM-Prebuilt-Root 14c5886 -- the end-to-end clean build that was accepted on device -- and its crosvm hash matches the binary that run produced. It carries the VNC listen address and the per-pool consume_system_mem accounting in crosvm, the drm2kgsl and venus error paths in virglrenderer, gfxstream's ColorBuffer import no longer aborting the VMM, a turnip rebuilt from the Banners fork, and edk2 from a serial BaseTools. The eight platform libraries are gone rather than updated: LD_LIBRARY_PATH searches the payload before /system/lib64, so shipping them shadowed the platform's and cost Android 17 the link entirely. nproc-guard arrives for all three KMIs.
padOctal produced 13 digits for entries of 64 GiB and more and field() cut the string to 12, so the header carried a silently wrong size. Write base-256 (0x80 flag, big-endian) there instead, the form TarReader now reads and GNU tar, bsdtar and Python's tarfile have always read. Sizes below 64 GiB are still 11/12-digit octal, byte for byte as before.
A VM accumulates things worth writing down -- what it is for, how to log in, what not to touch -- and a config field is the only place they stay attached to the VM rather than to whoever set it up. So VMConfig grows "notes", which rides along in a package like every other field: the notes are about the VM, not about the phone it happens to be on. They are edited on a page of their own. A row at the end of the editor's Basic section carries the first line and an Edit button, because prose in a row-sized text box is unusable the moment it is longer than a sentence. That page has three modes -- source, rendered, both -- in the title bar rather than under it, since a row of its own would cost the editor a line of text for a control that is three buttons wide. In "both", the split follows the screen's short axis: upright one pane above the other, on its side one beside the other, so each pane keeps the longest lines it can. It is taken from the container's own measurements rather than the display's, which keeps it right in split-screen, in freeform, and when the keyboard takes half the screen. The panes are sized in pixels, not weights, and that is not a detail: a weighted LinearLayout measures its weighted children once with an unbounded spec along the weight axis, and Compose throws outright when a horizontally scrollable element -- which is what this renderer makes of a table or a code block -- is measured against an infinite width. Upright that never happens; turning the phone took the screen down every time until the weights went. The rendered notes appear as a card on the VM's own page and on the vmpkg import screen, the latter being where someone handed a package finds out what they were handed, before importing it. Rendering is Compose, because the renderer is. It is confined to one file with one entry point, so the editor's preview and the read-only cards cannot drift apart -- they are the same call. Two things there are deliberate: the palette is read off the hosting Android theme, so a card of notes sits on the same surface colour as the card beside it instead of arriving in Compose purple; and the heading ladder is retuned for a phone, since the library starts h1 at displayLarge -- 57sp, right for a document on its own page and absurd in a card, where an h1 would take a line and a half on its own. The renderer itself is vendored rather than pulled as an artifact. Its published builds use a newer Kotlin than AGP's built-in Kotlin plugin provides, and a compiler refuses to read metadata from a newer one, which pinned us to 0.38.1 -- the last release built with Kotlin 2.2 -- and would have kept us there until AGP moved. As source it compiles with whatever Kotlin the build has, so the version becomes a choice: this takes 0.45.0, the current release, and it needs no change to compile under 2.2.10. What is taken is the renderer and its Material 3 bindings, commonMain only -- not the Coil loaders, the Material 2 bindings, the syntax-highlighting module, or the js/wasm/native source sets. 60 files, Apache-2.0, under app/src/main/vendor in a root of their own so that "not ours, and under its own licence" is structural rather than a convention, with no SPDX header of ours added to them. One file is changed and marked in place: MarkdownLogger declares `internal expect fun platformLog` upstream with an `actual` per platform, which needs the Kotlin Multiplatform plugin, so the androidMain implementation is inlined instead. The root is registered with kotlin.srcDir, not java.srcDir -- the latter compiles nothing here and leaves every reference unresolved, which is a confusing way to find out. Kotlin needs no plugin: AGP 9 compiles it out of the box and already owns the "kotlin" extension, so asking for the Kotlin plugin fails outright. Only the Compose compiler plugin is applied, pinned to the Kotlin AGP carries. What stays an artifact is what the renderer depends on -- the org.jetbrains markdown parser, kotlinx collections-immutable, coroutines and Compose -- all still subject to the metadata rule, and all currently built with an older Kotlin than ours. Signed-off-by: HuJK <gh@hujk.org>
…itor Two VMs on one image only corrupt it while both hold the file, so sharing alone is no longer a reason to take the write away from a slot the user configured: - the disk editor stops forcing read-only for a path another VM attaches. What it still forces is what its own rows can prove: a base with registered overlays, and two rows here pointing at the same file. - a pre-start guard asks the daemon instead. With every other VM on the disk stopped the start proceeds writable and untouched; with any of them not stopped it offers to attach those disks read-only, with the same 5s countdown the compressed-disk guard uses so an unattended start is not blocked. That flip lasts one boot: the daemon is handed a copy of the config and the store keeps its writable slots, so the next start decides again from what was saved. - deleting a VM now offers exactly the disks that deletion would leave dangling -- writable, and referenced by no other VM -- rather than counting every writable attachment and quietly keeping the shared ones afterwards. VmDiskSharing holds the one "who else attaches this path" rule the three sites now share, with the CDROM bus counting as unwritable everywhere: both backends open it read-only whatever the flag says, so it is neither at risk from a co-tenant nor a file the VM owns. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015BTc7yZ1DPCX7xpbYcUo2V
Each host module under Settings supplies something a particular configuration reaches for at run time, and without it the VM fails as anything but a missing module: a guest that cannot see its own RAM, GPU allocations failing at random, a big VM refusing to boot with "out of memory" on a phone with memory to spare. A pre-start guard asks up front, from what the config actually turns on -- pseudo-unprotected RAM wants gunyah_host_share, GPU acceleration wants gh_unmovable and udmabuf, and a VM over 2 GB wants the kvcalloc fix. Which of those apply to this phone is not decided here. The Kernel Module list already answers it -- the KMI directory picks the build for the running kernel and match.json drops what was written for another SoC -- so a module the list does not offer is skipped without a word rather than warned about. That is also why the kvcalloc rule needs no "and only on the 8 Gen 3": the fix is built for the 6.1 GKI whose Gunyah driver has the bug, and for Qualcomm, so it can only surface there. It keeps working as the module set grows to other vendors. The dialog offers to start anyway (5s countdown, the answer an unattended start gets), to open the module page, or to cancel. Nothing here loads a module on the user's behalf: that is a decision of its own, made where the modules are managed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015BTc7yZ1DPCX7xpbYcUo2V
…stead of crashing Stored values outlive the sets that produced them: an option gets retired between releases, and several graphics rows build their item set out of another row's value, so a config written under one combination is routinely read back under another. EnumPicker answered that with an exception, which made every restore path a crash waiting for the first user whose VM predates the current build -- and it arrived: an old VM naming a GL provider, under a backend whose set no longer lists one, took the editor down as it opened. setSelectedItem now lands an item the picker does not list, or lists only to refuse, on the row's default and returns whether the value itself was taken. setDefaultItem names that default wherever the head of the list is not the sensible answer -- a transport ladder whose head is the CPU copy should not be where "that rung is not built yet" ends up. Refused now means refused from every direction, a stored config included, so the picker no longer sits on a value it would not let the user pick; rotate-mode steps over refused rungs for the same reason. The graphics tab stops re-deriving which values belong to the current backend -- the copy of that rule at the call site is what did not cover the retired provider -- and hands each stored value to the picker that owns the set. If any row moved, the tab says so once: what is on screen is not what was saved, and the user should not have to notice that on their own. PanVK joins the same mechanism as a listed-and-refused item, replacing its toast-and-revert listener. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015BTc7yZ1DPCX7xpbYcUo2V
All five came out of a PR review of this branch. What they have in common is that each fails somewhere the message does not point at: a black screen with the stream still arriving, a boot that stops at an ioctl, a daemon that answers nothing for ten seconds, a prompt about memory a VM never touches. The review's other findings were checked and left alone -- either correct as they are, or a pre-existing limitation this branch did not introduce. vnc: the sync frame outlives the pipeline that was not there to take it. The server sends the parameter sets exactly once per client, on the reset-flagged rect that starts its stream, and the presentation console has nowhere to put that rect: it connects when it opens and builds its decoder only once a display has been chosen. The rect was dropped, every bare IDR after it decoded to nothing, and no reconnect followed because rects were still arriving. H264SyncFrameCache holds it for the connection instead of for the pipeline, which also covers the window being dismissed and rebuilt, and the race where DesktopSize reaches the main thread a post later than the sync rect reaches the message loop. hugepage: the reserve is asked about only the VMs that draw on it. The pre-start check sized every VM against pool_avail, including the ones on hypervisors that hand no memory away and cannot hit the failure it exists to avoid. They got the dialog, or ten seconds of waiting, for a pool their boot will not spend a page of. display: a refused socket path gives back the sockets bound before it. startListening() published its slots only after the last one bound, so the release() the caller already makes on that path freed an empty set -- leaking listening fds, inodes under run/, and an accept thread each, parked in accept(2) for the life of the daemon. Unreachable today; the two screen ids are spelt g0 and sfb precisely so their names stay short, and a third screen is what would arrive without a tag. The test pins the budget such a name would be chosen against. daemon: auto-start runs behind the socket, not in front of it. The sweep waits on the reserve from inside the ServerContext constructor, which is before the socket binds, before the signal handlers exist and before the VM event callback is wired. It runs from Server.run() on its own thread now, which is also why the sweep snapshots its list rather than iterating across the wait, why start() takes a lock from the state test to the worker, and why stopAll() can call the wait off: a daemon answering clients while the sweep runs is a sweep with company. vm: refuse to start a VM whose LEND parcels this kernel will not take. 6.6 demand-pages a parcel; 6.1 commits every one as it arrives and refuses a later one, so a 3 GB VM configured for 256 MB parcels does not boot there and says only "failed to initialize virtual machine". A better default would not have helped, because the value travels: an imported VM carries the mode of the phone its package came from. The dialog offers to correct it and keep the correction. Verified on a 6.1 Qualcomm device. The daemon bound its socket, logged the reserve wait 32 ms later, and authenticated a client 122 ms after that with nine seconds of waiting still to go; a SIGINT mid-wait was answered rather than queued behind it. The LEND dialog appeared on start, its fix wrote single to vms.json, the next crosvm command line carried --prepare-lend-mthp-mode single, and the VM reached RUNNING where it had been dying at GH_VM_START. 293 unit tests and the five CI source checks pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QzTPwWuSaDfFJg62dtqZW Signed-off-by: HuJK <gh@hujk.org>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
為 DroidVM 補上三條 GPU 路線與其所需的記憶體模型,並把顯示、磁碟、網路、週邊
的管理面補齊到可日常使用的程度。
本專案 自有的檔案 改以 GPL-2.0/3.0-or-later 授權,並附帶允許上游重新授權的補充許可。
新增
或 VNC,輸入裝置隨綁定走;傳輸管線(CPU / GPU copy / 零拷貝 / 硬體編碼)依匯出端
提供不同階梯
第一次開機的匯出端
127.0.0.1:0、SimpleFB:9),共用同一埠會在存檔前擋下-ov-yyMMdd-HHmmss),同一分鐘建兩個不會撞名就 share 完
寫的大小,比較好預測
看起來像映像壞了而不是網路選錯)
調整
優化
修正
/system/lib64path::__filename()的相容性