commit 53e66262313aaa3d0b7e95f8ef01747923aee0ac Author: Votre Nom Date: Fri May 8 17:41:55 2026 +0200 Initial commit: phases 1-3 du portage Wayland Rust pour Redox OS Plan directeur 14 phases / 5 ans (REDOX_COSMIC_XWAYLAND_RS_PLAN.md). Phase 1 — Audit Redox (docs/existing-redox-gui.md, 486 lignes) : - Orbital, graphics-ipc (API DRM compatible Linux subset KMS), inputd, vesad - relibc support : AF_UNIX, SCM_RIGHTS, shm_open, mmap, poll - 3 manques identifiés : memfd_create, keymap XKB, AT-SPI Phase 2 — Validation primitives sur Redox via redoxer (5 tests + 1 POC) : - test-unix-socket : SOCK_STREAM Wayland-shaped roundtrip - test-fd-passing : SCM_RIGHTS mono-process (artefact kernel) - test-fd-passing-fork : SCM_RIGHTS multi-process (validation Wayland critique) - test-shm-open : shm_open + mmap + persistance + unlink - test-poll-multifd : poll() multiplexing + POLLHUP - poc-pixels : datapath shm + SCM_RIGHTS bout en bout (10000 pixels ARGB) Phase 3 — wayland-rs sur Redox (compile + runtime) : - wayland-{scanner,backend,server,client} compilent pour x86_64-unknown-redox sans patch upstream (rustix supporte Redox via libc backend) - test-handshake : server/client wl_registry handshake roundtrip - test-shm-pipeline : pipeline complet (ListeningSocket Unix réel + fd passing via wl_shm.create_pool + wl_shm_pool + wl_buffer + wl_surface + commit + serveur lit pixels via fd reçu, validation pixel-perfect) Verdict phase 3 : wayland-rs upstream est viable sur Redox out-of-the-box, le port "Wayland sur Redox" est désormais un problème de compositor à écrire, pas de stack à porter. Prérequis build : redoxer (pas cargo direct, car CMSG_NXTHDR/CMSG_DATA ne sont pas linkés autrement vers librelibc.a). Leyoda 2026 – GPLv3 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..671099d --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Rust build artifacts +target/ +*/target/ +**/target/ +Cargo.lock + +# Generated test outputs +*.ppm +*.png +*.pcap + +# Editor / OS +.vscode/ +.idea/ +.DS_Store +*.swp +*.swo +*~ + +# Local environment +.env +.envrc diff --git a/README.md b/README.md new file mode 100644 index 0000000..7a97e32 --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# redox-wayland-compositor + +Portage de Wayland sur Redox OS, en Rust pur, visant à terme à remplacer +Orbital comme serveur graphique de base. + +**État** : phases 1+2+3 du plan directeur complétées le 2026-05-08. + +Le pipeline shm Wayland complet est validé end-to-end sur Redox via redoxer +(crate `redox-wl-test-shm-pipeline`) : socket Unix réel, `wl_compositor` + +`wl_shm` globals, fd passing via SCM_RIGHTS, `wl_shm_pool` + `wl_buffer`, +`wl_surface.attach`/`damage`/`commit`, lecture pixels côté serveur via le +fd reçu — pixel-perfect. + +## Structure + +``` +. +├── REDOX_COSMIC_XWAYLAND_RS_PLAN.md # plan directeur 14 phases / 5 ans +├── docs/ +│ ├── existing-redox-gui.md # phase 1 : audit Orbital + graphics-ipc + inputd + relibc +│ └── redox-wayland-primitives.md # phase 2 : 5 primitives + POC pixels validés sur Redox +├── crates/ +│ ├── redox-wl-test-unix-socket/ # primitive 1 : SOCK_STREAM Wayland-shaped +│ ├── redox-wl-test-fd-passing/ # primitive 2 : SCM_RIGHTS mono-process (artefact) +│ ├── redox-wl-test-fd-passing-fork/ # primitive 2b : SCM_RIGHTS via fork (vrai cas Wayland) +│ ├── redox-wl-test-shm-open/ # primitive 3 : shm_open + mmap MAP_SHARED +│ ├── redox-wl-test-poll-multifd/ # primitive 4 : poll() multiplexing + POLLHUP +│ ├── redox-wl-poc-pixels/ # POC : datapath shm + SCM_RIGHTS bout en bout +│ ├── redox-wl-test-handshake/ # phase 3 : wayland-rs server/client registry +│ └── redox-wl-test-shm-pipeline/ # phase 3 : pipeline shm complet validé +└── (pas de Cargo.toml racine : chaque crate est standalone — voir note ci-dessous) +``` + +> **Note structure** : pas de workspace Cargo racine. Chaque crate dans `crates/` +> est autonome avec son propre `Cargo.toml` et son propre `target/` lors de la build. +> Raison : `redoxer build`/`redoxer run` cherche le binaire dans `/target/...` +> et n'arrive pas à le retrouver si le `target/` est centralisé au niveau workspace. +> Ce sera repensé quand on aura un compositor unique vs plusieurs binaires de tests. + +## Prérequis + +- Rust nightly (récent — testé 2026-05-07) +- Target installé : `rustup target add x86_64-unknown-redox --toolchain nightly` +- `cargo install redoxer` puis `redoxer install` (sysroot Redox ~456 Mo) +- Repo upstream `Smithay/wayland-rs` cloné en `../wayland-rs/` (paths relatifs depuis `crates/*/Cargo.toml`) + +## Reproduire les tests + +Chaque crate s'exécute via redoxer (mini-VM Redox sous QEMU) : + +```bash +cd crates/redox-wl-test-shm-pipeline +redoxer run --release +``` + +Pattern attendu en fin d'exécution : `[test-XX] PASS: ...`. + +> **Important** : utiliser `redoxer build` ou `redoxer run`, **pas** `cargo build --target=x86_64-unknown-redox` direct +> qui fait un fail au link (`undefined reference CMSG_NXTHDR/CMSG_DATA`) parce que le linker host +> ne sait pas chaîner librelibc.a du sysroot Redox. `redoxer` configure le linker correctement. + +## Roadmap + +Voir `REDOX_COSMIC_XWAYLAND_RS_PLAN.md` pour le plan complet 14 phases / 5 ans. + +Prochaine étape : **phase 4 — display backend Redox via `graphics-ipc::V2GraphicsHandle`**. + +## Licence + +Leyoda 2026 — GPLv3+ diff --git a/REDOX_COSMIC_XWAYLAND_RS_PLAN.md b/REDOX_COSMIC_XWAYLAND_RS_PLAN.md new file mode 100644 index 0000000..75310d4 --- /dev/null +++ b/REDOX_COSMIC_XWAYLAND_RS_PLAN.md @@ -0,0 +1,892 @@ +# Plan global 3-5 ans : compositor Wayland Rust minimal pour remplacer Orbital + +## Document A Etudier + +Ce fichier est le document global de travail a etudier en priorite : + +```text +REDOX_COSMIC_XWAYLAND_RS_PLAN.md +``` + +Il doit servir de plan directeur pour avancer vers une GUI Redox moderne : + +- d'abord un compositor Wayland minimal en Rust qui remplace Orbital ; +- ensuite un backend plus robuste pour Smithay/COSMIC ; +- plus tard une compatibilite X11 via un serveur X11 Rust compatible Xwayland. + +## Vision + +Construire progressivement un compositor Wayland minimal, natif Redox, ecrit principalement en Rust, qui remplace Orbital comme serveur graphique de base. + +La cible initiale n'est pas COSMIC complet. La cible initiale est plus fondamentale : + +```text +Applications Wayland simples + | + v +Compositor Wayland minimal Rust + | + v +Backend Redox : inputd + graphics-ipc + schemes + | + v +vesad / framebuffer firmware au debut +GPU userspace Redox plus tard +``` + +La cible long terme est : + +```text +Applications Wayland natives / COSMIC +Applications X11 via serveur X11 Rust compatible Xwayland + | + v +Compositor Wayland Redox + | + v +Redox natif, sans Orbital +``` + +Pour etre complete, la GUI Redox doit couvrir trois manques structurels : + +- acceleration materielle via GPU ; +- support Wayland standard, ou protocole equivalent clairement documente ; +- accessibilite : lecteurs d'ecran, navigation clavier avancee, focus visible, raccourcis, contraste, zoom et APIs d'assistance. + +## Constat Sur L'Existant Redox + +### VESA / `vesad` + +`vesad` n'est pas vraiment un driver GPU. Il ecrit dans un framebuffer fourni par le firmware via UEFI ou BIOS. + +Implication pour le plan : + +- le premier compositor doit accepter un rendu framebuffer simple ; +- le rendu CPU reste obligatoire ; +- il ne faut pas attendre les drivers GPU pour commencer ; +- le backend display doit rester compatible avec un framebuffer firmware. + +### GPUs + +Redox n'a pas encore de pile GPU complete comparable a Linux/BSD. + +Sur Linux/BSD, la communication GPU passe par DRM dans le kernel, exposee par `libdrm`, puis utilisee par Mesa3D pour OpenGL/Vulkan. + +Dans Redox, un "driver DRM" doit plutot etre un daemon userspace qui parle au materiel via schemes et appels systeme. + +Implication pour le plan : + +- l'acceleration GPU est un objectif de maturite, pas un pre-requis du MVP ; +- le compositor doit separer clairement : + - protocole Wayland ; + - gestion surfaces ; + - rendu ; + - presentation display ; + - allocation buffers ; +- il faudra plus tard un backend Redox dans Mesa3D pour utiliser ces drivers userspace. + +### Software Rendering + +LLVMpipe fonctionne deja comme emulation OpenGL CPU. + +Implication pour le plan : + +- le rendu logiciel est le chemin initial naturel ; +- les clients OpenGL peuvent passer par LLVMpipe/OSMesa quand c'est possible ; +- l'objectif court terme n'est pas Vulkan/OpenGL accelere, mais des surfaces `wl_shm` fiables ; +- OSMesa et LLVMpipe doivent etre conserves comme ponts de compatibilite pendant la transition. + +### Orbital + +Orbital fournit actuellement : + +- display server ; +- window manager ; +- compositor ; +- launcher ; +- file manager ; +- text editor ; +- calculator ; +- terminal emulator ; +- raccourcis Super affiches en overlay ; +- protocole client via `orbclient`. + +Orbital est plus simple que X11/Wayland. Il est moins avance, mais suffisant pour porter beaucoup de programmes Linux/BSD via des bibliotheques adaptees. + +Bibliotheques deja utiles sur Orbital : + +- `winit` ; +- `softbuffer` ; +- Slint via `winit`/`softbuffer` ; +- Iced via `winit`/`softbuffer` ; +- egui via `winit` ou SDL2 ; +- SDL1.2 ; +- SDL2 ; +- Mesa3D OSMesa. + +Securite deja interessante : + +- un programme GUI Orbital ne peut pas lire les events input ou le framebuffer des autres fenetres ; +- ce modele est plus proche de Wayland que de X11. + +Implication pour le plan : + +- Orbital ne doit pas etre jete brutalement ; +- il sert de reference et de fallback pendant plusieurs annees ; +- les bibliotheques deja portees vers Orbital sont des candidats de migration vers Wayland Redox ; +- la securite type Wayland doit etre preservee ; +- `orbclient` ne doit pas devenir la nouvelle API long terme. + +## Strategie Corrigee + +### Mauvais Chemin + +Ne pas commencer par : + +- porter COSMIC complet ; +- reecrire Xwayland ; +- attendre les drivers GPU ; +- concevoir un protocole graphique maison concurrent de Wayland ; +- remplacer tout Orbital d'un coup. + +### Bon Chemin + +Commencer par : + +1. Valider les primitives Redox necessaires a Wayland. +2. Faire fonctionner `wayland-rs` sur Redox. +3. Ecrire un compositor Wayland minimal. +4. Reutiliser les idees d'Orbital pour display, input, damage, session et securite. +5. Rendre ce compositor capable de remplacer Orbital experimentalement. +6. Porter progressivement les bibliotheques et clients existants. +7. Preparer ensuite Smithay/COSMIC. +8. Traiter le serveur X11 Rust compatible Xwayland comme un projet long terme separe. + +## Objectif Principal + +Remplacer Orbital par un compositor Wayland natif Redox capable de : + +- prendre le controle de l'ecran sans Orbital ; +- afficher une ou plusieurs surfaces Wayland ; +- recevoir l'input via Redox ; +- gerer focus, clavier, souris, resize et damage ; +- fournir un protocole Wayland suffisamment standard pour porter des clients simples ; +- garder un rendu CPU fiable via framebuffer/LLVMpipe/OSMesa ; +- preparer une future acceleration GPU sans l'imposer au MVP ; +- preparer une couche d'accessibilite ; +- servir de base future a COSMIC. + +## Non-Objectifs Initiaux + +Ne pas viser dans le MVP : + +- COSMIC complet ; +- Xwayland ou equivalent X11 ; +- acceleration GPU obligatoire ; +- Mesa3D GPU complet ; +- Vulkan/OpenGL accelere ; +- multi-GPU ; +- HDR, color management avance, VRR ; +- portails desktop complets ; +- PipeWire/screencast ; +- decorations complexes ; +- compatibilite totale avec GTK/Qt/Electron ; +- lecteur d'ecran complet ; +- accessibilite desktop complete. + +Ces objectifs restent importants, mais ils viennent apres le compositor Wayland minimal. + +## Architecture Initiale + +```text +redox-wayland-compositor + | + +-- redox_display + | +-- graphics-ipc + | +-- vesad/framebuffer firmware + | +-- framebuffer CPU + | +-- damage flush + | +-- future DRM userspace driver daemon + | + +-- redox_input + | +-- inputd + | +-- keyboard + | +-- pointer + | +-- buttons + | +-- relative motion + | + +-- wayland_core + | +-- wl_display + | +-- wl_registry + | +-- wl_compositor + | +-- wl_surface + | +-- wl_shm + | +-- wl_output + | +-- wl_seat + | +-- xdg_shell plus tard + | + +-- compositor_core + | +-- surfaces + | +-- windows + | +-- focus + | +-- stacking + | +-- damage + | +-- frame callbacks + | + +-- renderer + | +-- CPU blit + | +-- alpha blend + | +-- cursor composition + | +-- LLVMpipe/OSMesa path for clients + | +-- GPU backend later + | + +-- accessibility + +-- keyboard navigation + +-- visible focus + +-- metadata windows/surfaces + +-- high contrast + +-- zoom later +``` + +## Relation Avec Orbital + +Orbital doit etre remplace progressivement, pas ignore. + +### A Reutiliser Comme Reference + +- [main.rs](main.rs) : demarrage via `VT`, lancement d'une commande de login/session. +- [core/display.rs](core/display.rs) : ouverture display, framebuffer CPU, cursor plane, `dirty_framebuffer`. +- [core/mod.rs](core/mod.rs) : event loop Redox, `inputd`, scheme. +- [compositor.rs](compositor.rs) : damage tracking, curseur, composition logicielle. +- [scheme.rs](scheme.rs) : focus, input routing, fenetres, raccourcis. +- [window.rs](window.rs) : modele de fenetre, buffers, flags, events. + +### A Conserver Pendant Transition + +- fallback Orbital dans les images de test ; +- applications Orbital essentielles ; +- bibliotheques deja portees : `winit`, `softbuffer`, Slint, Iced, egui, SDL, OSMesa ; +- experience utilisateur de base : terminal, launcher, raccourcis. + +### A Remplacer + +- protocole `orbital:` ; +- API client `orbclient` comme interface graphique principale ; +- mapping direct des fenetres via `mmap_prep` Orbital ; +- semantics de fenetres propres a Orbital ; +- hypothese qu'une seule implementation serveur suffit pour tous les clients. + +## Roadmap Globale + +| Phase | Nom | Objectif | +| --- | --- | --- | +| 0 | Cadrage | depot, build, documentation, perimetre | +| 1 | Audit existant Redox | vesad, graphics-ipc, inputd, Orbital, LLVMpipe | +| 2 | Primitives Redox | sockets, fd passing, shm, mmap, poll | +| 3 | Wayland-rs minimal | serveur Wayland + client test | +| 4 | Display backend | affichage Redox sans Orbital | +| 5 | Input backend | clavier/souris via inputd | +| 6 | Surfaces shm | affichage de surfaces Wayland | +| 7 | Compositor utilisable | focus, stacking, damage, cursor | +| 8 | Remplacement Orbital experimental | boot Redox vers compositor Wayland | +| 9 | Migration bibliotheques | winit, softbuffer, SDL, clients simples | +| 10 | Stabilisation desktop | xdg-shell, clipboard, output, tests | +| 11 | Accessibilite initiale | navigation clavier, focus visible, metadata UI | +| 12 | Acceleration GPU | abstraction renderer, backend GPU experimental | +| 13 | Pont vers Smithay/COSMIC | preparation port COSMIC | +| 14 | Compat X11 Rust | projet separe apres socle stable | + +## Phase 0 : Cadrage + +### Objectif + +Creer un cadre de travail clair. + +### Taches + +- Creer un depot ou sous-depot `redox-wayland-compositor`. +- Definir les crates : + - `redox-wl-compositor` + - `redox-wl-display` + - `redox-wl-input` + - `redox-wl-render` + - `redox-wl-accessibility` + - `redox-wl-protocol-tests` +- Definir les clients de test. +- Definir la strategie de fallback Orbital. +- Definir la matrice de compatibilite. + +### Validation + +Un binaire minimal compile et s'integre dans l'environnement de build Redox. + +## Phase 1 : Audit De L'Existant Redox + +### Objectif + +Comprendre exactement ce qui existe avant d'ecrire le nouveau compositor. + +### Taches + +- Lire le code Orbital actuel. +- Lire le fonctionnement de `graphics-ipc`. +- Lire le fonctionnement de `inputd`. +- Identifier le chemin exact vers `vesad`. +- Identifier comment LLVMpipe/OSMesa sont integres. +- Lister les bibliotheques deja fonctionnelles sur Orbital. +- Lister les APIs Redox disponibles pour sockets, fd passing, shm, mmap. +- Documenter les differences avec Linux. + +### Livrables + +- `docs/existing-redox-gui.md` +- schema de la stack actuelle ; +- liste des composants reutilisables ; +- liste des blocages. + +### Validation + +Le projet sait expliquer comment Orbital affiche une fenetre aujourd'hui et comment le nouveau compositor remplacera chaque partie. + +## Phase 2 : Primitives Systeme Redox Pour Wayland + +### Objectif + +Valider les mecanismes necessaires au protocole Wayland. + +### Primitives critiques + +- Unix domain sockets ; +- passage de file descriptors ; +- memoire partagee ; +- `mmap` ; +- `poll` ou event loop equivalente ; +- timers ; +- gestion des processus clients. + +### Taches + +- Ecrire tests serveur/client socket. +- Ecrire tests fd passing. +- Ecrire tests shm/mmap. +- Ecrire tests event loop multi-fd. +- Corriger ou completer Redox/relibc si necessaire. + +### Validation + +Un serveur peut recevoir un client, lui envoyer une zone memoire partagee, detecter des changements et traiter plusieurs fd dans une boucle d'evenements. + +## Phase 3 : Wayland-rs Minimal Sur Redox + +### Objectif + +Faire tourner un serveur Wayland minimal et un client Wayland minimal. + +### Protocoles prioritaires + +- `wl_display` +- `wl_registry` +- `wl_compositor` +- `wl_surface` +- `wl_shm` +- `wl_buffer` +- `wl_output` +- `wl_seat` +- `wl_keyboard` +- `wl_pointer` + +### Taches + +- Porter ou adapter `wayland-backend`. +- Porter ou adapter `wayland-server`. +- Porter ou adapter `wayland-client`. +- Generer les bindings necessaires. +- Ecrire un client Wayland test. + +### Validation + +Un client Wayland simple peut creer une surface shm et envoyer un commit au serveur. + +## Phase 4 : Backend Display Redox + +### Objectif + +Ouvrir l'ecran Redox et dessiner sans Orbital. + +### Taches + +- Creer `RedoxOutput`. +- Ouvrir le display via les primitives Redox existantes. +- Supporter le framebuffer CPU fourni par le chemin firmware/vesad. +- Implementer clear screen. +- Implementer draw rectangle. +- Implementer flush damage. +- Implementer resize si possible. +- Ajouter curseur logiciel initial. + +### Validation + +Un binaire prend l'ecran sans Orbital et affiche une scene simple. + +## Phase 5 : Backend Input Redox + +### Objectif + +Recevoir clavier, souris et boutons via `inputd`. + +### Taches + +- Ouvrir le consumer input Redox. +- Lire clavier. +- Lire souris absolue. +- Lire souris relative. +- Lire boutons et scroll. +- Normaliser les coordonnees. +- Router l'input vers une surface. + +### Validation + +Le compositor recoit l'input et peut deplacer un curseur ou focaliser une surface. + +## Phase 6 : Surfaces Wayland SHM + +### Objectif + +Afficher de vraies surfaces Wayland basees sur `wl_shm`. + +### Formats initiaux + +- `XRGB8888` +- `ARGB8888` + +### Taches + +- Recevoir buffers `wl_shm`. +- Gerer `attach`. +- Gerer `damage`. +- Gerer `commit`. +- Composer les surfaces dans le framebuffer Redox. +- Envoyer `frame callbacks`. + +### Validation + +Une surface Wayland shm s'affiche et se met a jour correctement. + +## Phase 7 : Compositor Utilisable Minimal + +### Objectif + +Passer d'une demo a un mini desktop experimental. + +### Fonctionnalites + +- stacking ; +- focus clavier ; +- focus pointer ; +- move de fenetres ; +- resize simple ; +- curseur ; +- damage tracking ; +- frame scheduling ; +- fermeture client ; +- raccourcis globaux. + +### Validation + +Deux clients Wayland simples s'affichent, recoivent input, peuvent etre focalises et deplaces. + +## Phase 8 : Remplacement Experimental d'Orbital + +### Objectif + +Demarrer Redox directement sur le compositor Wayland minimal. + +### Taches + +- Integrer le compositor dans le boot/session. +- Definir `WAYLAND_DISPLAY`. +- Lancer un client Wayland test au demarrage. +- Garder une option fallback Orbital. +- Documenter la procedure de retour arriere. + +### Validation + +Orbital n'est plus necessaire pour afficher le premier client graphique de test. + +## Phase 9 : Migration Des Bibliotheques Existantes + +### Objectif + +Utiliser la force de l'existant Redox/Orbital pour alimenter le nouveau monde Wayland. + +### Taches + +- Auditer `winit` Redox/Orbital. +- Ajouter ou adapter un backend Wayland Redox pour `winit`. +- Auditer `softbuffer`. +- Tester Slint, Iced et egui sur le nouveau chemin. +- Tester SDL2 Wayland si possible. +- Conserver OSMesa/LLVMpipe pour les clients qui ont besoin d'OpenGL CPU. + +### Validation + +Au moins une application Rust via `winit` ou `softbuffer` s'affiche dans le compositor Wayland Redox. + +## Phase 10 : Stabilisation Desktop Minimal + +### Objectif + +Rendre le remplacement d'Orbital credible pour le developpement experimental. + +### Fonctionnalites + +- `xdg-shell` basique ; +- clipboard Wayland minimal ; +- multi-fenetre robuste ; +- resize output ; +- key repeat ; +- keymap simple ; +- logs structures ; +- tests pixels ; +- tests input. + +### Validation + +Des clients Wayland non triviaux peuvent tourner sans adaptation lourde. + +## Phase 11 : Accessibilite Initiale + +### Objectif + +Faire de l'accessibilite une capacite native du desktop Redox. + +### Fonctionnalites prioritaires + +- navigation clavier complete dans le shell minimal ; +- focus visible ; +- ordre de focus stable ; +- raccourcis configurables ; +- contraste eleve ; +- taille de curseur configurable ; +- zoom logiciel basique ; +- metadata de fenetres/surfaces. + +### Validation + +La session minimale peut etre utilisee sans souris pour lancer, focaliser, deplacer et fermer une application. + +## Phase 12 : Acceleration GPU Experimentale + +### Objectif + +Preparer la transition vers l'acceleration materielle sans casser le rendu CPU. + +### Taches + +- Definir un trait `Renderer`. +- Garder le renderer CPU comme reference. +- Ajouter abstraction de buffers : + - shm CPU ; + - futur buffer GPU ; + - import/export futur. +- Etudier : + - drivers DRM userspace Redox ; + - backend Redox Mesa3D ; + - LLVMpipe comme transition ; + - virtio-gpu ; + - wgpu si viable ; + - renderer Rust dedie si necessaire. + +### Validation + +Le compositor peut choisir entre renderer CPU et backend experimental sans changer sa logique Wayland. + +## Phase 13 : Preparation Smithay/COSMIC + +### Objectif + +Transformer les acquis du compositor minimal en backend reusable pour Smithay/COSMIC. + +### Taches + +- Extraire un backend Redox propre : + - display ; + - input ; + - session ; + - renderer logiciel. +- Porter une demo type `smallvil` ou `anvil`. +- Auditer `cosmic-comp`. +- Remplacer les dependances Linux : + - libinput ; + - udev ; + - libseat/logind ; + - systemd ; + - EGL/GBM si absent. + +### Validation + +Smithay ou un `cosmic-comp` reduit peut utiliser les primitives developpees. + +## Phase 14 : Horizon Serveur X11 Rust Compatible Xwayland + +### Objectif + +Apres stabilisation Wayland, lancer un projet separe de serveur X11 Rust. + +### MVP X11 + +- handshake X11 ; +- table de ressources ; +- `CreateWindow` ; +- `MapWindow` ; +- `DestroyWindow` ; +- `ConfigureWindow` ; +- `PutImage` ; +- `CopyArea` ; +- events clavier/souris ; +- `Expose` ; +- fenetre top-level transformee en surface Wayland. + +### Extensions ensuite + +- BIG-REQUESTS ; +- MIT-SHM ; +- XFIXES ; +- DAMAGE ; +- RENDER ; +- RANDR minimal ; +- XInput2 minimal ; +- Composite plus tard ; +- GLX tres tard. + +### Validation + +Une application X11 simple peut ouvrir une fenetre, dessiner et recevoir input dans la session Wayland Redox. + +## Jalons Annuels Indicatifs + +### Annee 1 : Audit Et Fondations + +Livrables : + +- audit complet de l'existant Redox GUI ; +- tests fd passing/shm/mmap ; +- serveur Wayland minimal ; +- client Wayland test ; +- backend display affichant des pixels sans Orbital. + +Validation : + +- le socle systeme est compris ; +- une surface shm peut etre recue ; +- l'ecran peut etre controle dans une demo hors Orbital. + +### Annee 2 : Premier Compositor + +Livrables : + +- input Redox ; +- surfaces shm composees ; +- curseur ; +- focus ; +- damage ; +- plusieurs fenetres simples. + +Validation : + +- deux clients Wayland simples s'affichent et recoivent input. + +### Annee 3 : Remplacement Experimental d'Orbital + +Livrables : + +- integration session ; +- terminal minimal ; +- launcher minimal ; +- clipboard texte initial ; +- `xdg-shell` basique ; +- navigation clavier ; +- focus visible. + +Validation : + +- Orbital n'est plus requis pour une session graphique experimentale. + +### Annee 4 : Stabilisation Et Migration + +Livrables : + +- multi-fenetre robuste ; +- tests automatiques ; +- keymap/repeat ; +- migration `winit`/`softbuffer` ou equivalent ; +- mode contraste eleve ; +- zoom logiciel basique ; +- trait renderer CPU/GPU. + +Validation : + +- au moins une app issue de l'ecosysteme existant Redox/Orbital tourne sur le nouveau compositor. + +### Annee 5 : Smithay/COSMIC/GPU + +Livrables : + +- backend Smithay Redox avance ; +- premiere execution de `cosmic-comp` reduit ou demo equivalente ; +- backend GPU experimental ou rapport technique documentant les blocages ; +- service accessibilite experimental ; +- debut du serveur X11 Rust seulement si le socle est stable. + +Validation : + +- decision claire : continuer compositor maison, migrer vers Smithay, ou brancher COSMIC directement ; +- decision claire sur le chemin GPU : Mesa/DRM userspace, virtio-gpu, wgpu, renderer Rust dedie, ou CPU optimise. + +## Definition Du MVP + +Le MVP est atteint quand : + +- Redox peut demarrer une session graphique sans Orbital ; +- le compositor Wayland minimal prend l'ecran ; +- un client Wayland shm s'affiche ; +- clavier et souris fonctionnent ; +- plusieurs surfaces peuvent coexister ; +- le focus fonctionne ; +- le damage fonctionne ; +- une application minimale type terminal ou launcher peut etre lancee ; +- la navigation clavier minimale fonctionne ; +- le focus visible est implemente. + +## Definition Du Remplacement Orbital + +Orbital peut etre considere remplace experimentalement quand : + +- le boot graphique peut utiliser le compositor Wayland dans une image de test ; +- les applications de base ont une alternative Wayland ; +- le debug reste possible depuis un terminal ; +- le compositor peut etre quitte ou redemarre proprement ; +- un fallback Orbital existe pendant la transition ; +- l'utilisateur peut effectuer les actions principales au clavier. + +Orbital peut etre considere remplace fonctionnellement quand : + +- les composants desktop essentiels sont disponibles en Wayland ; +- les tests de session passent regulierement ; +- la gestion input/output est stable ; +- la memoire partagee ne fuit pas ; +- les crashs clients ne tuent pas le compositor ; +- les bases d'accessibilite sont presentes ; +- une strategie GPU est implementee ou documentee avec backend experimental. + +## Risques Majeurs + +### Primitives OS Insuffisantes + +Wayland depend fortement de sockets, fd passing, shm et mmap. + +Mitigation : + +- traiter cette phase avant tout port COSMIC ; +- maintenir des tests bas niveau. + +### GPU Trop Immature + +Redox n'a pas encore de drivers GPU complets. + +Mitigation : + +- renderer CPU obligatoire ; +- LLVMpipe/OSMesa comme pont ; +- interface renderer stable ; +- backend GPU experimental seulement apres stabilisation. + +### Scope Trop Large + +COSMIC et X11 peuvent absorber plusieurs annees. + +Mitigation : + +- les exclure du MVP ; +- avancer par clients Wayland simples. + +### Divergence Avec Wayland Standard + +Un compositor maison peut devenir incompatible. + +Mitigation : + +- utiliser `wayland-rs` ; +- tester avec clients externes ; +- implementer `xdg-shell`. + +### Accessibilite Oubliee + +Un desktop impossible a utiliser sans souris serait incomplet. + +Mitigation : + +- navigation clavier dans le MVP ; +- focus visible obligatoire ; +- metadata accessibles definies tot. + +## Crates Proposees + +```text +crates/ + redox-wl-compositor/ + redox-wl-display/ + redox-wl-input/ + redox-wl-render/ + redox-wl-accessibility/ + redox-wl-protocol-tests/ +``` + +## Documents A Produire Ensuite + +Apres ce plan global, les documents utiles seront : + +```text +docs/existing-redox-gui.md +docs/redox-wayland-primitives.md +docs/compositor-architecture.md +docs/display-backend-redox.md +docs/input-backend-redox.md +docs/software-rendering-and-gpu-roadmap.md +docs/accessibility-roadmap.md +docs/orbital-migration.md +docs/cosmic-smithay-roadmap.md +docs/x11-compat-rust-roadmap.md +``` + +## Ordre D'Etude Recommande + +1. Lire ce document global en entier. +2. Lire le code Orbital actuel : + - `main.rs` + - `core/display.rs` + - `core/mod.rs` + - `compositor.rs` + - `scheme.rs` + - `window.rs` +3. Etudier `graphics-ipc` et `inputd`. +4. Etudier `vesad` et le chemin framebuffer Redox. +5. Etudier comment LLVMpipe/OSMesa sont integres. +6. Etudier `wayland-rs`, surtout `wayland-backend`, `wayland-server`, `wayland-client`. +7. Etudier ensuite Smithay. +8. Etudier COSMIC seulement apres les bases Wayland Redox. + +## Regle De Progression + +Chaque phase doit produire : + +- un binaire testable ; +- une demo visible ; +- des logs exploitables ; +- au moins un test automatique quand possible ; +- une note de limitations. + +Ne pas passer a COSMIC tant que le compositor minimal ne peut pas remplacer Orbital dans une image experimentale. + +Ne pas lancer le serveur X11 Rust tant que le compositor Wayland ne sait pas gerer correctement surfaces, focus, damage et input. + diff --git a/crates/redox-wl-poc-pixels/Cargo.toml b/crates/redox-wl-poc-pixels/Cargo.toml new file mode 100644 index 0000000..1dafcc8 --- /dev/null +++ b/crates/redox-wl-poc-pixels/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "redox-wl-poc-pixels" +version = "0.1.0" +edition = "2021" + +[dependencies] +libc = "0.2" diff --git a/crates/redox-wl-poc-pixels/src/main.rs b/crates/redox-wl-poc-pixels/src/main.rs new file mode 100644 index 0000000..11e7f7f --- /dev/null +++ b/crates/redox-wl-poc-pixels/src/main.rs @@ -0,0 +1,310 @@ +//! POC — preuve par les pixels. +//! +//! Démontre concrètement que le datapath Wayland fonctionne sur Redox : +//! - un CLIENT crée une zone shm, dessine un pattern ARGB déterministe +//! - il envoie le fd au SERVEUR via Unix socket + SCM_RIGHTS +//! - le SERVEUR mappe la zone, vérifie chaque pixel, écrit un PPM +//! +//! C'est exactement ce que fait wl_shm + wl_buffer entre un client Wayland +//! et son compositor, sans le protocole Wayland lui-même. Si ce POC passe, +//! la phase 3 (port wayland-rs) peut démarrer en confiance. +//! +//! Mono-binaire qui fork() : le child fait le client, le parent le serveur. + +use std::ffi::CString; +use std::io::{self, Write}; +use std::mem::{self, MaybeUninit}; +use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}; +use std::process::ExitCode; +use std::ptr; + +const W: usize = 200; +const H: usize = 200; +const PIXEL_BYTES: usize = 4; +const SHM_BYTES: usize = W * H * PIXEL_BYTES; +const SHM_NAME: &str = "/redox-wl-poc-pixels"; +const PPM_OUTPUT: &str = "poc-shm-pixels-readback.ppm"; + +/// Pattern ARGB8888 déterministe : channel A = 0xFF, R = x, G = y, B = (x+y) mod 256. +fn expected_pixel(x: usize, y: usize) -> u32 { + let a: u32 = 0xFF; + let r: u32 = (x as u32) & 0xFF; + let g: u32 = (y as u32) & 0xFF; + let b: u32 = ((x + y) as u32) & 0xFF; + (a << 24) | (r << 16) | (g << 8) | b +} + +fn errno_str() -> String { + let e = io::Error::last_os_error(); + format!("{e} (errno={})", e.raw_os_error().unwrap_or(0)) +} + +unsafe fn make_socketpair() -> Result<(OwnedFd, OwnedFd), String> { + let mut fds: [RawFd; 2] = [-1, -1]; + if libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, fds.as_mut_ptr()) != 0 { + return Err(format!("socketpair: {}", errno_str())); + } + Ok((OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1]))) +} + +unsafe fn send_fd_over(socket: RawFd, fd_to_send: RawFd) -> Result<(), String> { + let mut iov_buf = [b'P'; 1]; + let mut iov = libc::iovec { + iov_base: iov_buf.as_mut_ptr() as *mut _, + iov_len: 1, + }; + let cmsg_space = libc::CMSG_SPACE(mem::size_of::() as u32) as usize; + let mut cmsg_buf = vec![0u8; cmsg_space]; + let mut msg: libc::msghdr = mem::zeroed(); + msg.msg_iov = &mut iov as *mut _; + msg.msg_iovlen = 1; + msg.msg_control = cmsg_buf.as_mut_ptr() as *mut _; + msg.msg_controllen = cmsg_space as _; + let cmsg = libc::CMSG_FIRSTHDR(&msg); + (*cmsg).cmsg_level = libc::SOL_SOCKET; + (*cmsg).cmsg_type = libc::SCM_RIGHTS; + (*cmsg).cmsg_len = libc::CMSG_LEN(mem::size_of::() as u32) as _; + let data_ptr = libc::CMSG_DATA(cmsg) as *mut RawFd; + ptr::write_unaligned(data_ptr, fd_to_send); + + let n = libc::sendmsg(socket, &msg, 0); + if n < 0 { + return Err(format!("sendmsg: {}", errno_str())); + } + Ok(()) +} + +unsafe fn recv_fd_from(socket: RawFd) -> Result { + let mut iov_buf = [0u8; 1]; + let mut iov = libc::iovec { + iov_base: iov_buf.as_mut_ptr() as *mut _, + iov_len: 1, + }; + let cmsg_space = libc::CMSG_SPACE(mem::size_of::() as u32) as usize; + let mut cmsg_buf = vec![MaybeUninit::::uninit(); cmsg_space]; + let mut msg: libc::msghdr = mem::zeroed(); + msg.msg_iov = &mut iov as *mut _; + msg.msg_iovlen = 1; + msg.msg_control = cmsg_buf.as_mut_ptr() as *mut _; + msg.msg_controllen = cmsg_space as _; + + let n = libc::recvmsg(socket, &mut msg, 0); + if n < 0 { + return Err(format!("recvmsg: {}", errno_str())); + } + let cmsg = libc::CMSG_FIRSTHDR(&msg); + if cmsg.is_null() { + return Err("no cmsg received".into()); + } + if (*cmsg).cmsg_level != libc::SOL_SOCKET || (*cmsg).cmsg_type != libc::SCM_RIGHTS { + return Err("unexpected cmsg type".into()); + } + let data_ptr = libc::CMSG_DATA(cmsg) as *const RawFd; + let received_fd = ptr::read_unaligned(data_ptr); + Ok(OwnedFd::from_raw_fd(received_fd)) +} + +unsafe fn shm_create_and_map(name: &str, size: usize) -> Result<(OwnedFd, *mut u8), String> { + let cname = CString::new(name).unwrap(); + let fd = libc::shm_open(cname.as_ptr(), libc::O_RDWR | libc::O_CREAT, 0o600); + if fd < 0 { + return Err(format!("shm_open: {}", errno_str())); + } + if libc::ftruncate(fd, size as _) != 0 { + libc::close(fd); + return Err(format!("ftruncate: {}", errno_str())); + } + let p = libc::mmap( + ptr::null_mut(), + size, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_SHARED, + fd, + 0, + ); + if p == libc::MAP_FAILED { + libc::close(fd); + return Err(format!("mmap (create): {}", errno_str())); + } + Ok((OwnedFd::from_raw_fd(fd), p as *mut u8)) +} + +unsafe fn mmap_existing(fd: RawFd, size: usize) -> Result<*mut u8, String> { + let p = libc::mmap( + ptr::null_mut(), + size, + libc::PROT_READ, + libc::MAP_SHARED, + fd, + 0, + ); + if p == libc::MAP_FAILED { + return Err(format!("mmap (recv): {}", errno_str())); + } + Ok(p as *mut u8) +} + +unsafe fn child_main(child_sock: OwnedFd) -> ! { + eprintln!("[poc CLIENT pid={}] start", libc::getpid()); + + // Crée la zone shm + let (shm_fd, map) = match shm_create_and_map(SHM_NAME, SHM_BYTES) { + Ok(v) => v, + Err(e) => { + eprintln!("[poc CLIENT] FAIL shm_create_and_map: {e}"); + libc::_exit(2); + } + }; + eprintln!( + "[poc CLIENT] shm '{}' size={} mapped at {:p} fd={}", + SHM_NAME, SHM_BYTES, map, shm_fd.as_raw_fd() + ); + + // Dessine le pattern ARGB déterministe + let pixels = std::slice::from_raw_parts_mut(map as *mut u32, W * H); + for y in 0..H { + for x in 0..W { + pixels[y * W + x] = expected_pixel(x, y); + } + } + eprintln!( + "[poc CLIENT] painted {}x{} ARGB pattern (a=0xFF, r=x, g=y, b=(x+y)&0xFF)", + W, H + ); + + // Envoie le fd au serveur + if let Err(e) = send_fd_over(child_sock.as_raw_fd(), shm_fd.as_raw_fd()) { + eprintln!("[poc CLIENT] FAIL sendmsg: {e}"); + libc::_exit(3); + } + eprintln!("[poc CLIENT] sent shm fd via SCM_RIGHTS"); + + // On peut munmap + close côté client : le serveur a son propre ref via SCM_RIGHTS + libc::munmap(map as *mut _, SHM_BYTES); + drop(shm_fd); + + eprintln!("[poc CLIENT] done, exit 0"); + libc::_exit(0); +} + +unsafe fn parent_main(parent_sock: OwnedFd, child_pid: libc::pid_t) -> Result<(), String> { + eprintln!("[poc SERVER pid={}] waiting for fd from child {}", libc::getpid(), child_pid); + + let received = recv_fd_from(parent_sock.as_raw_fd())?; + let recv_raw = received.as_raw_fd(); + eprintln!("[poc SERVER] received fd={recv_raw}"); + + let map = mmap_existing(recv_raw, SHM_BYTES)?; + eprintln!("[poc SERVER] mmap'd received fd at {:p}", map); + + // Vérifie chaque pixel + let pixels = std::slice::from_raw_parts(map as *const u32, W * H); + let mut mismatches: Vec<(usize, usize, u32, u32)> = Vec::new(); + for y in 0..H { + for x in 0..W { + let got = pixels[y * W + x]; + let want = expected_pixel(x, y); + if got != want { + if mismatches.len() < 5 { + mismatches.push((x, y, got, want)); + } + } + } + } + if !mismatches.is_empty() { + for (x, y, got, want) in &mismatches { + eprintln!( + "[poc SERVER] MISMATCH at ({x},{y}): got {got:#010x} want {want:#010x}" + ); + } + return Err(format!("{} pixel mismatches over {}", mismatches.len(), W * H)); + } + eprintln!( + "[poc SERVER] all {} pixels match expected pattern", + W * H + ); + + // Bonus : dump en PPM pour visualisation (P6 binaire RGB) + match write_ppm(PPM_OUTPUT, pixels, W, H) { + Ok(()) => eprintln!("[poc SERVER] PPM dump written to {PPM_OUTPUT}"), + Err(e) => eprintln!("[poc SERVER] WARN: PPM dump failed: {e}"), + } + + libc::munmap(map as *mut _, SHM_BYTES); + drop(received); + + // Reap child + let mut status: libc::c_int = 0; + let r = libc::waitpid(child_pid, &mut status, 0); + if r < 0 { + return Err(format!("waitpid: {}", errno_str())); + } + if !libc::WIFEXITED(status) || libc::WEXITSTATUS(status) != 0 { + return Err(format!( + "child exited abnormally: status={status}" + )); + } + eprintln!("[poc SERVER] child reaped cleanly"); + + // Cleanup shm + let cname = CString::new(SHM_NAME).unwrap(); + libc::shm_unlink(cname.as_ptr()); + + Ok(()) +} + +fn write_ppm(path: &str, pixels: &[u32], w: usize, h: usize) -> io::Result<()> { + let mut f = std::fs::File::create(path)?; + write!(f, "P6\n{w} {h}\n255\n")?; + let mut row = vec![0u8; w * 3]; + for y in 0..h { + for x in 0..w { + let argb = pixels[y * w + x]; + let r = ((argb >> 16) & 0xFF) as u8; + let g = ((argb >> 8) & 0xFF) as u8; + let b = (argb & 0xFF) as u8; + row[x * 3] = r; + row[x * 3 + 1] = g; + row[x * 3 + 2] = b; + } + f.write_all(&row)?; + } + Ok(()) +} + +fn run() -> Result<(), String> { + println!( + "[poc] preuve par les pixels: client {}x{} ARGB → SCM_RIGHTS → serveur", + W, H + ); + + unsafe { + let (sock_parent, sock_child) = make_socketpair()?; + + let pid = libc::fork(); + if pid < 0 { + return Err(format!("fork: {}", errno_str())); + } + if pid == 0 { + drop(sock_parent); + child_main(sock_child); + } + + drop(sock_child); + parent_main(sock_parent, pid)?; + } + Ok(()) +} + +fn main() -> ExitCode { + match run() { + Ok(()) => { + println!("[poc] PASS: full Wayland-like datapath validated on Redox"); + ExitCode::SUCCESS + } + Err(e) => { + eprintln!("[poc] FAIL: {e}"); + ExitCode::FAILURE + } + } +} diff --git a/crates/redox-wl-test-fd-passing-fork/Cargo.toml b/crates/redox-wl-test-fd-passing-fork/Cargo.toml new file mode 100644 index 0000000..833390c --- /dev/null +++ b/crates/redox-wl-test-fd-passing-fork/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "redox-wl-test-fd-passing-fork" +version = "0.1.0" +edition = "2021" + +[dependencies] +libc = "0.2" diff --git a/crates/redox-wl-test-fd-passing-fork/src/main.rs b/crates/redox-wl-test-fd-passing-fork/src/main.rs new file mode 100644 index 0000000..d7b5fed --- /dev/null +++ b/crates/redox-wl-test-fd-passing-fork/src/main.rs @@ -0,0 +1,215 @@ +//! Test 2b — fd passing via SCM_RIGHTS, multi-process via fork(). +//! +//! Mono-process test (test-02) revealed the kernel reuses fd numbers and +//! does not duplicate the file table entry. This test validates whether +//! SCM_RIGHTS works correctly *across* a process boundary, which is the +//! actual Wayland use case. +//! +//! Flow: +//! 1. Parent opens a tmp file with a marker +//! 2. socketpair(AF_UNIX, SOCK_STREAM) +//! 3. fork() +//! 4. Parent: closes child end, sendmsg with the tmp_fd +//! 5. Child: closes parent end, recvmsg, reads via received fd, exits with status 0 if marker matches +//! 6. Parent: waitpid, returns child's exit status + +use std::ffi::CString; +use std::io; +use std::mem::{self, MaybeUninit}; +use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}; +use std::process::ExitCode; +use std::ptr; + +const MARKER: &[u8] = b"REDOX-FORK-FD-PASSING-OK-2026"; + +fn errno_str() -> String { + let e = io::Error::last_os_error(); + format!("{e} (errno={})", e.raw_os_error().unwrap_or(0)) +} + +unsafe fn make_socketpair() -> Result<(OwnedFd, OwnedFd), String> { + let mut fds: [RawFd; 2] = [-1, -1]; + if libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, fds.as_mut_ptr()) != 0 { + return Err(format!("socketpair: {}", errno_str())); + } + Ok((OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1]))) +} + +unsafe fn open_tmp_with_marker() -> Result { + let path = CString::new("/tmp/test-02b-fd-passing-fork.bin").unwrap(); + let fd = libc::open( + path.as_ptr(), + libc::O_RDWR | libc::O_CREAT | libc::O_TRUNC, + 0o600, + ); + if fd < 0 { + return Err(format!("open tmp: {}", errno_str())); + } + let n = libc::write(fd, MARKER.as_ptr() as *const _, MARKER.len()); + if n != MARKER.len() as isize { + return Err(format!("write tmp: {}", errno_str())); + } + libc::lseek(fd, 0, libc::SEEK_SET); + Ok(OwnedFd::from_raw_fd(fd)) +} + +unsafe fn send_fd_over(socket: RawFd, fd_to_send: RawFd) -> Result<(), String> { + let mut iov_buf = [b'X'; 1]; + let mut iov = libc::iovec { + iov_base: iov_buf.as_mut_ptr() as *mut _, + iov_len: 1, + }; + let cmsg_space = libc::CMSG_SPACE(mem::size_of::() as u32) as usize; + let mut cmsg_buf = vec![0u8; cmsg_space]; + let mut msg: libc::msghdr = mem::zeroed(); + msg.msg_iov = &mut iov as *mut _; + msg.msg_iovlen = 1; + msg.msg_control = cmsg_buf.as_mut_ptr() as *mut _; + msg.msg_controllen = cmsg_space as _; + + let cmsg = libc::CMSG_FIRSTHDR(&msg); + (*cmsg).cmsg_level = libc::SOL_SOCKET; + (*cmsg).cmsg_type = libc::SCM_RIGHTS; + (*cmsg).cmsg_len = libc::CMSG_LEN(mem::size_of::() as u32) as _; + let data_ptr = libc::CMSG_DATA(cmsg) as *mut RawFd; + ptr::write_unaligned(data_ptr, fd_to_send); + + let n = libc::sendmsg(socket, &msg, 0); + if n < 0 { + return Err(format!("sendmsg: {}", errno_str())); + } + Ok(()) +} + +unsafe fn recv_fd_from(socket: RawFd) -> Result { + let mut iov_buf = [0u8; 1]; + let mut iov = libc::iovec { + iov_base: iov_buf.as_mut_ptr() as *mut _, + iov_len: 1, + }; + let cmsg_space = libc::CMSG_SPACE(mem::size_of::() as u32) as usize; + let mut cmsg_buf = vec![MaybeUninit::::uninit(); cmsg_space]; + let mut msg: libc::msghdr = mem::zeroed(); + msg.msg_iov = &mut iov as *mut _; + msg.msg_iovlen = 1; + msg.msg_control = cmsg_buf.as_mut_ptr() as *mut _; + msg.msg_controllen = cmsg_space as _; + + let n = libc::recvmsg(socket, &mut msg, 0); + if n < 0 { + return Err(format!("recvmsg: {}", errno_str())); + } + + let cmsg = libc::CMSG_FIRSTHDR(&msg); + if cmsg.is_null() { + return Err("no cmsg".into()); + } + if (*cmsg).cmsg_level != libc::SOL_SOCKET || (*cmsg).cmsg_type != libc::SCM_RIGHTS { + return Err(format!( + "unexpected cmsg level={} type={}", + (*cmsg).cmsg_level, + (*cmsg).cmsg_type + )); + } + let data_ptr = libc::CMSG_DATA(cmsg) as *const RawFd; + let received_fd = ptr::read_unaligned(data_ptr); + Ok(OwnedFd::from_raw_fd(received_fd)) +} + +unsafe fn child_main(child_sock: OwnedFd) -> ! { + let raw_sock = child_sock.as_raw_fd(); + eprintln!("[test-02b CHILD pid={}] recvmsg on sock {}", libc::getpid(), raw_sock); + let received = match recv_fd_from(raw_sock) { + Ok(fd) => fd, + Err(e) => { + eprintln!("[test-02b CHILD] recvmsg fail: {e}"); + libc::_exit(2); + } + }; + let recv_raw = received.as_raw_fd(); + eprintln!("[test-02b CHILD] received fd={recv_raw}"); + + libc::lseek(recv_raw, 0, libc::SEEK_SET); + let mut readback = vec![0u8; MARKER.len()]; + let n = libc::read(recv_raw, readback.as_mut_ptr() as *mut _, readback.len()); + if n != MARKER.len() as isize { + eprintln!( + "[test-02b CHILD] read returned {n}, errno={}", + io::Error::last_os_error().raw_os_error().unwrap_or(0) + ); + libc::_exit(3); + } + if readback != MARKER { + eprintln!( + "[test-02b CHILD] marker mismatch: {:?}", + String::from_utf8_lossy(&readback) + ); + libc::_exit(4); + } + eprintln!("[test-02b CHILD] marker matches, exit 0"); + libc::_exit(0); +} + +fn run() -> Result<(), String> { + println!("[test-02b] fd passing via SCM_RIGHTS across fork()"); + + unsafe { + let (sock_parent, sock_child) = make_socketpair()?; + let tmp_fd = open_tmp_with_marker()?; + println!("[test-02b PARENT] tmp fd={}, sockets {}/{}", + tmp_fd.as_raw_fd(), sock_parent.as_raw_fd(), sock_child.as_raw_fd()); + + let pid = libc::fork(); + if pid < 0 { + return Err(format!("fork: {}", errno_str())); + } + if pid == 0 { + // Child: drop tmp_fd, drop parent socket, run child_main on its own socket + drop(tmp_fd); + drop(sock_parent); + child_main(sock_child); + } + + // Parent path + drop(sock_child); + println!("[test-02b PARENT] forked child pid={pid}, sending fd"); + send_fd_over(sock_parent.as_raw_fd(), tmp_fd.as_raw_fd())?; + drop(tmp_fd); + drop(sock_parent); + + let mut status: libc::c_int = 0; + let r = libc::waitpid(pid, &mut status, 0); + if r < 0 { + return Err(format!("waitpid: {}", errno_str())); + } + let exited = libc::WIFEXITED(status); + let code = libc::WEXITSTATUS(status); + println!("[test-02b PARENT] child reaped: exited={exited} code={code}"); + + if !exited { + return Err("child did not exit normally".into()); + } + if code != 0 { + return Err(format!("child exited with status {code}")); + } + + // Cleanup tmp file + let path = std::ffi::CString::new("/tmp/test-02b-fd-passing-fork.bin").unwrap(); + libc::unlink(path.as_ptr()); + } + + Ok(()) +} + +fn main() -> ExitCode { + match run() { + Ok(()) => { + println!("[test-02b] PASS: fd passed across fork() correctly"); + ExitCode::SUCCESS + } + Err(e) => { + eprintln!("[test-02b] FAIL: {e}"); + ExitCode::FAILURE + } + } +} diff --git a/crates/redox-wl-test-fd-passing/Cargo.toml b/crates/redox-wl-test-fd-passing/Cargo.toml new file mode 100644 index 0000000..adaf255 --- /dev/null +++ b/crates/redox-wl-test-fd-passing/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "redox-wl-test-fd-passing" +version = "0.1.0" +edition = "2021" + +[dependencies] +libc = "0.2" diff --git a/crates/redox-wl-test-fd-passing/src/main.rs b/crates/redox-wl-test-fd-passing/src/main.rs new file mode 100644 index 0000000..86c479a --- /dev/null +++ b/crates/redox-wl-test-fd-passing/src/main.rs @@ -0,0 +1,195 @@ +//! Test 2 — fd passing via sendmsg/recvmsg with SCM_RIGHTS. +//! +//! This is THE critical primitive for Wayland: clients pass shm fds +//! (and dmabuf fds) to the compositor through this mechanism. +//! +//! relibc Redox (platform/redox/socket.rs:272,447,835,924) declares +//! support — this test validates the actual behaviour. +//! +//! Flow: +//! 1. socketpair(AF_UNIX, SOCK_STREAM) +//! 2. open a tmp file, write a known marker into it +//! 3. sendmsg the fd via SCM_RIGHTS through socket A +//! 4. recvmsg on socket B, extract the fd +//! 5. read from the received fd, check marker matches +//! +//! Single-process test (no fork needed): we send the fd to ourselves +//! through the socketpair to validate the mechanism works end-to-end. + +use std::ffi::CString; +use std::io; +use std::mem::{self, MaybeUninit}; +use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}; +use std::process::ExitCode; +use std::ptr; + +const MARKER: &[u8] = b"REDOX-FD-PASSING-OK-2026"; + +fn errno_str() -> String { + let e = io::Error::last_os_error(); + format!("{e} (errno={})", e.raw_os_error().unwrap_or(0)) +} + +unsafe fn make_socketpair() -> Result<(OwnedFd, OwnedFd), String> { + let mut fds: [RawFd; 2] = [-1, -1]; + let r = libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, fds.as_mut_ptr()); + if r != 0 { + return Err(format!("socketpair: {}", errno_str())); + } + Ok((OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1]))) +} + +unsafe fn open_tmp_with_marker() -> Result { + let path = CString::new("/tmp/test-02-fd-passing.bin").unwrap(); + let fd = libc::open( + path.as_ptr(), + libc::O_RDWR | libc::O_CREAT | libc::O_TRUNC, + 0o600, + ); + if fd < 0 { + return Err(format!("open tmp: {}", errno_str())); + } + let n = libc::write(fd, MARKER.as_ptr() as *const _, MARKER.len()); + if n != MARKER.len() as isize { + return Err(format!("write tmp: {}", errno_str())); + } + libc::lseek(fd, 0, libc::SEEK_SET); + Ok(OwnedFd::from_raw_fd(fd)) +} + +unsafe fn send_fd_over(socket: RawFd, fd_to_send: RawFd) -> Result<(), String> { + let mut iov_buf = [b'X'; 1]; + let mut iov = libc::iovec { + iov_base: iov_buf.as_mut_ptr() as *mut _, + iov_len: 1, + }; + + let cmsg_space = libc::CMSG_SPACE(mem::size_of::() as u32) as usize; + let mut cmsg_buf = vec![0u8; cmsg_space]; + + let mut msg: libc::msghdr = mem::zeroed(); + msg.msg_iov = &mut iov as *mut _; + msg.msg_iovlen = 1; + msg.msg_control = cmsg_buf.as_mut_ptr() as *mut _; + msg.msg_controllen = cmsg_space as _; + + let cmsg = libc::CMSG_FIRSTHDR(&msg); + if cmsg.is_null() { + return Err("CMSG_FIRSTHDR null".into()); + } + (*cmsg).cmsg_level = libc::SOL_SOCKET; + (*cmsg).cmsg_type = libc::SCM_RIGHTS; + (*cmsg).cmsg_len = libc::CMSG_LEN(mem::size_of::() as u32) as _; + let data_ptr = libc::CMSG_DATA(cmsg) as *mut RawFd; + ptr::write_unaligned(data_ptr, fd_to_send); + + let n = libc::sendmsg(socket, &msg, 0); + if n < 0 { + return Err(format!("sendmsg: {}", errno_str())); + } + println!("[test-02] sendmsg sent {n} bytes + 1 fd via SCM_RIGHTS"); + Ok(()) +} + +unsafe fn recv_fd_from(socket: RawFd) -> Result { + let mut iov_buf = [0u8; 1]; + let mut iov = libc::iovec { + iov_base: iov_buf.as_mut_ptr() as *mut _, + iov_len: 1, + }; + + let cmsg_space = libc::CMSG_SPACE(mem::size_of::() as u32) as usize; + let mut cmsg_buf = vec![MaybeUninit::::uninit(); cmsg_space]; + + let mut msg: libc::msghdr = mem::zeroed(); + msg.msg_iov = &mut iov as *mut _; + msg.msg_iovlen = 1; + msg.msg_control = cmsg_buf.as_mut_ptr() as *mut _; + msg.msg_controllen = cmsg_space as _; + + let n = libc::recvmsg(socket, &mut msg, 0); + if n < 0 { + return Err(format!("recvmsg: {}", errno_str())); + } + println!("[test-02] recvmsg got {n} bytes + control of {} bytes", msg.msg_controllen); + + let cmsg = libc::CMSG_FIRSTHDR(&msg); + if cmsg.is_null() { + return Err("no cmsg in received message".into()); + } + if (*cmsg).cmsg_level != libc::SOL_SOCKET || (*cmsg).cmsg_type != libc::SCM_RIGHTS { + return Err(format!( + "unexpected cmsg level={} type={}", + (*cmsg).cmsg_level, + (*cmsg).cmsg_type + )); + } + let data_ptr = libc::CMSG_DATA(cmsg) as *const RawFd; + let received_fd = ptr::read_unaligned(data_ptr); + if received_fd < 0 { + return Err(format!("received negative fd: {received_fd}")); + } + Ok(OwnedFd::from_raw_fd(received_fd)) +} + +fn run() -> Result<(), String> { + println!("[test-02] fd passing via SCM_RIGHTS"); + + unsafe { + let (sock_a, sock_b) = make_socketpair()?; + let tmp_fd = open_tmp_with_marker()?; + println!("[test-02] tmp fd = {}", tmp_fd.as_raw_fd()); + + // Strategy: send a dup'd fd, then close BOTH the original and the dup. + // If SCM_RIGHTS works per POSIX, the received fd is an independent + // reference to the open file and must remain readable. + let dup_fd = libc::dup(tmp_fd.as_raw_fd()); + if dup_fd < 0 { + return Err(format!("dup: {}", errno_str())); + } + println!("[test-02] dup'd tmp_fd ({}) -> {dup_fd}", tmp_fd.as_raw_fd()); + + send_fd_over(sock_a.as_raw_fd(), dup_fd)?; + let received = recv_fd_from(sock_b.as_raw_fd())?; + let recv_raw = received.as_raw_fd(); + println!("[test-02] received fd = {recv_raw}"); + + // Close BOTH the original and the dup. If SCM_RIGHTS gave us a real + // duplicate, the received fd must survive. + let orig_raw = tmp_fd.as_raw_fd(); + drop(tmp_fd); + libc::close(dup_fd); + println!("[test-02] closed original fd ({orig_raw}) and dup ({dup_fd}); reading via received only"); + + libc::lseek(recv_raw, 0, libc::SEEK_SET); + let mut readback = vec![0u8; MARKER.len()]; + let n = libc::read(recv_raw, readback.as_mut_ptr() as *mut _, readback.len()); + if n != MARKER.len() as isize { + return Err(format!("read from received fd: only {n} bytes (errno {})", + io::Error::last_os_error().raw_os_error().unwrap_or(0))); + } + if readback != MARKER { + return Err(format!("marker mismatch: got {:?}", String::from_utf8_lossy(&readback))); + } + println!("[test-02] read {} bytes via received fd: marker matches", n); + + // Cleanup tmp file + let path = std::ffi::CString::new("/tmp/test-02-fd-passing.bin").unwrap(); + libc::unlink(path.as_ptr()); + } + + Ok(()) +} + +fn main() -> ExitCode { + match run() { + Ok(()) => { + println!("[test-02] PASS: fd passing works, received fd reads correct marker"); + ExitCode::SUCCESS + } + Err(e) => { + eprintln!("[test-02] FAIL: {e}"); + ExitCode::FAILURE + } + } +} diff --git a/crates/redox-wl-test-handshake/Cargo.toml b/crates/redox-wl-test-handshake/Cargo.toml new file mode 100644 index 0000000..6e78431 --- /dev/null +++ b/crates/redox-wl-test-handshake/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "redox-wl-test-handshake" +version = "0.1.0" +edition = "2021" + +[dependencies] +wayland-server = { path = "../../../wayland-rs/wayland-server", default-features = false } +wayland-client = { path = "../../../wayland-rs/wayland-client", default-features = false } +wayland-backend = { path = "../../../wayland-rs/wayland-backend", default-features = false } diff --git a/crates/redox-wl-test-handshake/src/main.rs b/crates/redox-wl-test-handshake/src/main.rs new file mode 100644 index 0000000..904410f --- /dev/null +++ b/crates/redox-wl-test-handshake/src/main.rs @@ -0,0 +1,200 @@ +//! Test 5 — Wayland handshake server/client sur Redox. +//! +//! Valide que wayland-rs (server + client + backend) ne se contente pas de +//! compiler pour Redox mais fonctionne réellement à l'exécution : +//! +//! 1. Serveur Wayland local + globals (wl_compositor, wl_shm) +//! 2. socketpair AF_UNIX, on insère le côté serveur via insert_client +//! 3. Client Wayland connecté sur l'autre côté via WAYLAND_SOCKET fd env +//! 4. Client fait wl_display.get_registry, dispatch +//! 5. On vérifie que les globals attendus arrivent côté client +//! +//! Si ça passe, le port wayland-rs sur Redox est validé runtime, pas juste +//! compile-time. + +use std::os::unix::net::UnixStream; +use std::process::ExitCode; +use std::sync::Arc; + +use wayland_client::{ + Connection as ClientConnection, Dispatch as ClientDispatch, EventQueue, + backend::Backend as ClientBackend, + protocol::wl_registry, +}; +use wayland_server::{ + Display as ServerDisplay, GlobalDispatch, New, + backend::ClientData, + protocol::{wl_compositor, wl_shm}, +}; + +// ---- Server side ---- + +struct ServerState; + +#[derive(Debug)] +struct DumbClientData; +impl ClientData for DumbClientData {} + +impl GlobalDispatch for ServerState { + fn bind( + _state: &mut Self, + _handle: &wayland_server::DisplayHandle, + _client: &wayland_server::Client, + _resource: New, + _data: &(), + _data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + } +} + +impl wayland_server::Dispatch for ServerState { + fn request( + _state: &mut Self, + _client: &wayland_server::Client, + _resource: &wl_compositor::WlCompositor, + _request: wl_compositor::Request, + _data: &(), + _dh: &wayland_server::DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + } +} + +impl GlobalDispatch for ServerState { + fn bind( + _state: &mut Self, + _handle: &wayland_server::DisplayHandle, + _client: &wayland_server::Client, + _resource: New, + _data: &(), + _data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + } +} + +impl wayland_server::Dispatch for ServerState { + fn request( + _state: &mut Self, + _client: &wayland_server::Client, + _resource: &wl_shm::WlShm, + _request: wl_shm::Request, + _data: &(), + _dh: &wayland_server::DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + } +} + +// ---- Client side ---- + +#[derive(Default, Debug)] +struct ClientState { + globals: Vec<(u32, String, u32)>, + done: bool, +} + +impl ClientDispatch for ClientState { + fn event( + state: &mut Self, + _registry: &wl_registry::WlRegistry, + event: wl_registry::Event, + _data: &(), + _conn: &ClientConnection, + _qhandle: &wayland_client::QueueHandle, + ) { + if let wl_registry::Event::Global { name, interface, version } = event { + println!("[test-05 CLIENT] global #{name}: {interface} v{version}"); + state.globals.push((name, interface, version)); + } + } +} + +fn run() -> Result<(), Box> { + println!("[test-05] wayland-rs handshake server/client on Redox"); + + // -- Server setup -- + let mut server_display: ServerDisplay = ServerDisplay::new()?; + let mut dh = server_display.handle(); + dh.create_global::(5, ()); + dh.create_global::(1, ()); + println!("[test-05 SERVER] created wl_compositor v5 + wl_shm v1 globals"); + + // -- Socket linking -- + let (s_server, s_client) = UnixStream::pair()?; + s_server.set_nonblocking(true)?; + let _client_handle = dh.insert_client(s_server, Arc::new(DumbClientData))?; + println!("[test-05] linked server side via insert_client"); + + // -- Client setup using the other end of the socketpair -- + let client_backend = ClientBackend::connect(s_client)?; + let client_conn = ClientConnection::from_backend(client_backend); + let mut event_queue: EventQueue = client_conn.new_event_queue(); + let qhandle = event_queue.handle(); + let _registry = client_conn.display().get_registry(&qhandle, ()); + println!("[test-05 CLIENT] called get_registry, dispatching..."); + + let mut server_state = ServerState; + let mut client_state = ClientState::default(); + + // -- Roundtrip: dispatch alternately until client has events -- + for round in 0..10 { + // server side + server_display.dispatch_clients(&mut server_state)?; + server_display.flush_clients()?; + // client side + let _ = event_queue.flush(); + let processed = event_queue.dispatch_pending(&mut client_state)?; + if processed > 0 { + println!("[test-05] round {round}: client processed {processed} event(s)"); + } + if !client_state.globals.is_empty() { + client_state.done = true; + break; + } + // Block briefly waiting for incoming events on client side + match event_queue.prepare_read() { + Some(read_guard) => { + let _ = read_guard.read(); + } + None => {} + } + } + + if !client_state.done || client_state.globals.is_empty() { + return Err(format!( + "client did not receive globals (got {})", + client_state.globals.len() + ) + .into()); + } + + let names: Vec<&str> = client_state.globals.iter().map(|(_, n, _)| n.as_str()).collect(); + let has_compositor = names.contains(&"wl_compositor"); + let has_shm = names.contains(&"wl_shm"); + + if !has_compositor || !has_shm { + return Err(format!("missing expected globals: got {:?}", names).into()); + } + + println!( + "[test-05] received {} global(s): {:?}", + client_state.globals.len(), + names + ); + + Ok(()) +} + +fn main() -> ExitCode { + match run() { + Ok(()) => { + println!("[test-05] PASS: wayland-rs server/client handshake works on Redox"); + ExitCode::SUCCESS + } + Err(e) => { + eprintln!("[test-05] FAIL: {e}"); + ExitCode::FAILURE + } + } +} + diff --git a/crates/redox-wl-test-poll-multifd/Cargo.toml b/crates/redox-wl-test-poll-multifd/Cargo.toml new file mode 100644 index 0000000..32ff628 --- /dev/null +++ b/crates/redox-wl-test-poll-multifd/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "redox-wl-test-poll-multifd" +version = "0.1.0" +edition = "2021" + +[dependencies] +libc = "0.2" diff --git a/crates/redox-wl-test-poll-multifd/src/main.rs b/crates/redox-wl-test-poll-multifd/src/main.rs new file mode 100644 index 0000000..43d2f44 --- /dev/null +++ b/crates/redox-wl-test-poll-multifd/src/main.rs @@ -0,0 +1,120 @@ +//! Test 4 — poll() over multiple fds. +//! +//! A Wayland compositor multiplexes many fds: the listening socket, +//! each connected client, the input device(s), idle timers, signals. +//! This test validates that poll() correctly demultiplexes. +//! +//! Setup: 3 pipes (a, b, c). We write into b only, then poll all 3. +//! Only fd_b should report POLLIN. We read it. Then we close a, expecting +//! POLLHUP on a-read side. Finally we test timeout-only behaviour. + +use std::io; +use std::os::fd::RawFd; +use std::process::ExitCode; + +fn errno_str() -> String { + let e = io::Error::last_os_error(); + format!("{e} (errno={})", e.raw_os_error().unwrap_or(0)) +} + +unsafe fn make_pipe() -> Result<(RawFd, RawFd), String> { + let mut fds: [RawFd; 2] = [-1, -1]; + if libc::pipe(fds.as_mut_ptr()) != 0 { + return Err(format!("pipe: {}", errno_str())); + } + Ok((fds[0], fds[1])) +} + +unsafe fn poll_revents(fds: &mut [libc::pollfd], timeout_ms: i32) -> Result { + let r = libc::poll(fds.as_mut_ptr(), fds.len() as _, timeout_ms); + if r < 0 { + return Err(format!("poll: {}", errno_str())); + } + Ok(r) +} + +fn run() -> Result<(), String> { + println!("[test-04] poll() multi-fd multiplexing"); + + unsafe { + // 3 pipes; we'll only watch the read ends. + let (ra, wa) = make_pipe()?; + let (rb, wb) = make_pipe()?; + let (rc, wc) = make_pipe()?; + println!("[test-04] pipes: ra={ra} rb={rb} rc={rc}"); + + // ---- Subtest A: timeout-only, no fd ready ---- + let mut pfds = [ + libc::pollfd { fd: ra, events: libc::POLLIN, revents: 0 }, + libc::pollfd { fd: rb, events: libc::POLLIN, revents: 0 }, + libc::pollfd { fd: rc, events: libc::POLLIN, revents: 0 }, + ]; + let n = poll_revents(&mut pfds, 50)?; + if n != 0 { + return Err(format!("expected timeout (0), got {n}")); + } + println!("[test-04] A: timeout 50ms with no data → poll returned 0 OK"); + + // ---- Subtest B: write into pipe B only ---- + let payload = b"X"; + let n = libc::write(wb, payload.as_ptr() as *const _, 1); + if n != 1 { + return Err(format!("write to wb: {}", errno_str())); + } + + for p in pfds.iter_mut() { p.revents = 0; } + let n = poll_revents(&mut pfds, 1000)?; + if n != 1 { + return Err(format!("expected 1 ready fd, got {n}")); + } + let ready: Vec<_> = pfds.iter().enumerate() + .filter(|(_, p)| p.revents & libc::POLLIN != 0) + .map(|(i, _)| i) + .collect(); + if ready != [1] { + return Err(format!("expected only index 1 (rb) ready, got {ready:?}")); + } + println!("[test-04] B: poll detected only rb (index 1) ready, as expected"); + + let mut buf = [0u8; 1]; + let r = libc::read(rb, buf.as_mut_ptr() as *mut _, 1); + if r != 1 || buf[0] != b'X' { + return Err(format!("read rb: r={r} buf={:?}", buf)); + } + println!("[test-04] B: read 1 byte 'X' from rb OK"); + + // ---- Subtest C: close write end of A, expect HUP on read end ---- + libc::close(wa); + for p in pfds.iter_mut() { p.revents = 0; } + let n = poll_revents(&mut pfds, 100)?; + if n < 1 { + return Err(format!("expected at least 1 ready (POLLHUP on ra), got {n}")); + } + let hup_a = pfds[0].revents & (libc::POLLHUP | libc::POLLIN) != 0; + if !hup_a { + return Err(format!("expected HUP/IN on ra, revents={:#x}", pfds[0].revents)); + } + println!("[test-04] C: closing wa caused POLLHUP/POLLIN on ra (revents={:#x})", + pfds[0].revents); + + // Cleanup + libc::close(ra); + libc::close(rb); libc::close(wb); + libc::close(rc); libc::close(wc); + } + + Ok(()) +} + +fn main() -> ExitCode { + match run() { + Ok(()) => { + println!("[test-04] PASS: poll() multiplexes correctly across fds and detects HUP"); + ExitCode::SUCCESS + } + Err(e) => { + eprintln!("[test-04] FAIL: {e}"); + ExitCode::FAILURE + } + } +} diff --git a/crates/redox-wl-test-shm-open/Cargo.toml b/crates/redox-wl-test-shm-open/Cargo.toml new file mode 100644 index 0000000..8cec7bb --- /dev/null +++ b/crates/redox-wl-test-shm-open/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "redox-wl-test-shm-open" +version = "0.1.0" +edition = "2021" + +[dependencies] +libc = "0.2" diff --git a/crates/redox-wl-test-shm-open/src/main.rs b/crates/redox-wl-test-shm-open/src/main.rs new file mode 100644 index 0000000..395b51d --- /dev/null +++ b/crates/redox-wl-test-shm-open/src/main.rs @@ -0,0 +1,142 @@ +//! Test 3 — shm_open + mmap MAP_SHARED. +//! +//! Wayland clients use shm_open + ftruncate + mmap to create wl_shm pools. +//! The compositor then receives the fd via SCM_RIGHTS and mmaps the same region. +//! +//! relibc Redox maps shm_open("/foo") to open("/scheme/shm/foo"). +//! This test validates: +//! 1. shm_open with O_RDWR | O_CREAT works +//! 2. ftruncate sets the size +//! 3. mmap MAP_SHARED returns a writable region +//! 4. writes are persistent: close + reopen + remap reads back the data +//! 5. shm_unlink removes the segment +//! +//! Two passes (write then read-back) simulate the producer/consumer pattern +//! used by Wayland: client writes pixels, compositor reads them. + +use std::ffi::CString; +use std::io; +use std::os::fd::RawFd; +use std::process::ExitCode; +use std::ptr; + +const SHM_NAME: &str = "/redox-wl-test-03"; +const SIZE: usize = 4096; + +fn errno_str() -> String { + let e = io::Error::last_os_error(); + format!("{e} (errno={})", e.raw_os_error().unwrap_or(0)) +} + +unsafe fn shm_open_rw_create(name: &str, mode: libc::mode_t) -> Result { + let cname = CString::new(name).unwrap(); + let fd = libc::shm_open( + cname.as_ptr(), + libc::O_RDWR | libc::O_CREAT, + mode as _, + ); + if fd < 0 { + return Err(format!("shm_open({name}): {}", errno_str())); + } + Ok(fd) +} + +unsafe fn shm_open_rw(name: &str) -> Result { + let cname = CString::new(name).unwrap(); + let fd = libc::shm_open(cname.as_ptr(), libc::O_RDWR, 0); + if fd < 0 { + return Err(format!("shm_open(reopen {name}): {}", errno_str())); + } + Ok(fd) +} + +unsafe fn map_rw(fd: RawFd, size: usize) -> Result<*mut u8, String> { + let p = libc::mmap( + ptr::null_mut(), + size, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_SHARED, + fd, + 0, + ); + if p == libc::MAP_FAILED { + return Err(format!("mmap MAP_SHARED: {}", errno_str())); + } + Ok(p as *mut u8) +} + +fn run() -> Result<(), String> { + println!("[test-03] shm_open + mmap MAP_SHARED"); + + unsafe { + // Phase 1: create + write + let fd1 = shm_open_rw_create(SHM_NAME, 0o600)?; + println!("[test-03] phase1: shm_open created, fd={fd1}"); + + if libc::ftruncate(fd1, SIZE as _) != 0 { + return Err(format!("ftruncate: {}", errno_str())); + } + + let map1 = map_rw(fd1, SIZE)?; + println!("[test-03] phase1: mmap at {:p}", map1); + + // Write a recognisable pattern (ARGB8888 simulation: 256 pixels of 0xDEADBEEF) + let pixels = std::slice::from_raw_parts_mut(map1 as *mut u32, SIZE / 4); + for (i, p) in pixels.iter_mut().enumerate() { + *p = 0xDEAD_BEEF_u32.wrapping_add(i as u32); + } + println!("[test-03] phase1: wrote {} u32 pixels", pixels.len()); + + if libc::munmap(map1 as *mut _, SIZE) != 0 { + return Err(format!("munmap phase1: {}", errno_str())); + } + libc::close(fd1); + + // Phase 2: reopen + check pattern + let fd2 = shm_open_rw(SHM_NAME)?; + println!("[test-03] phase2: shm_open reopened, fd={fd2}"); + + let map2 = map_rw(fd2, SIZE)?; + println!("[test-03] phase2: mmap at {:p}", map2); + + let pixels2 = std::slice::from_raw_parts(map2 as *const u32, SIZE / 4); + for (i, &p) in pixels2.iter().enumerate() { + let expected = 0xDEAD_BEEF_u32.wrapping_add(i as u32); + if p != expected { + libc::munmap(map2 as *mut _, SIZE); + libc::close(fd2); + let cname = CString::new(SHM_NAME).unwrap(); + libc::shm_unlink(cname.as_ptr()); + return Err(format!("pixel {i} mismatch: got {p:#x}, expected {expected:#x}")); + } + } + println!("[test-03] phase2: pattern verified across close/reopen"); + + if libc::munmap(map2 as *mut _, SIZE) != 0 { + return Err(format!("munmap phase2: {}", errno_str())); + } + libc::close(fd2); + + // Cleanup + let cname = CString::new(SHM_NAME).unwrap(); + if libc::shm_unlink(cname.as_ptr()) != 0 { + return Err(format!("shm_unlink: {}", errno_str())); + } + println!("[test-03] cleanup: shm_unlink OK"); + } + + Ok(()) +} + +fn main() -> ExitCode { + match run() { + Ok(()) => { + println!("[test-03] PASS: shm_open/mmap/persistence/unlink all work"); + ExitCode::SUCCESS + } + Err(e) => { + eprintln!("[test-03] FAIL: {e}"); + ExitCode::FAILURE + } + } +} diff --git a/crates/redox-wl-test-shm-pipeline/Cargo.toml b/crates/redox-wl-test-shm-pipeline/Cargo.toml new file mode 100644 index 0000000..83667d0 --- /dev/null +++ b/crates/redox-wl-test-shm-pipeline/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "redox-wl-test-shm-pipeline" +version = "0.1.0" +edition = "2021" + +[dependencies] +wayland-server = { path = "../../../wayland-rs/wayland-server", default-features = false } +wayland-client = { path = "../../../wayland-rs/wayland-client", default-features = false } +wayland-backend = { path = "../../../wayland-rs/wayland-backend", default-features = false } +libc = "0.2" diff --git a/crates/redox-wl-test-shm-pipeline/src/main.rs b/crates/redox-wl-test-shm-pipeline/src/main.rs new file mode 100644 index 0000000..0927315 --- /dev/null +++ b/crates/redox-wl-test-shm-pipeline/src/main.rs @@ -0,0 +1,589 @@ +//! Test 6 — Pipeline complet wayland-rs sur Redox. +//! +//! C'est ce que fait un compositor Wayland minimal : +//! - serveur expose un socket Unix (ListeningSocket) +//! - client se connecte via WAYLAND_SOCKET (vrai pattern Wayland) +//! - client crée wl_shm_pool depuis un fd shm +//! - client crée wl_buffer depuis le pool, peint un pattern ARGB +//! - client crée wl_surface, attach, damage, commit +//! - serveur reçoit le commit, mappe le buffer fd, lit les pixels, vérifie +//! +//! Si ce test passe, le pipeline shm Wayland complet fonctionne sur Redox. +//! C'est la preuve technique que la phase 4-7 du plan directeur (display +//! backend + input + surfaces shm + compositor minimal) est réalisable +//! sans surprise majeure. + +use std::ffi::CString; +use std::io; +use std::os::fd::{AsFd, AsRawFd, BorrowedFd, IntoRawFd, OwnedFd}; +use std::os::unix::net::{UnixListener, UnixStream}; +use std::process::ExitCode; +use std::ptr; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; + +use wayland_client::{ + Connection as ClientConnection, Dispatch as ClientDispatch, EventQueue, Proxy as _, + QueueHandle as ClientQueueHandle, + backend::Backend as ClientBackend, + protocol::{ + wl_buffer::WlBuffer as ClientBuffer, wl_compositor::WlCompositor as ClientCompositor, + wl_registry, wl_shm::WlShm as ClientShm, wl_shm_pool::WlShmPool as ClientShmPool, + wl_surface::WlSurface as ClientSurface, + }, +}; +use wayland_server::{ + Client, DataInit, Display as ServerDisplay, DisplayHandle, GlobalDispatch, + backend::ClientData, + protocol::{ + wl_buffer, wl_callback, wl_compositor, wl_shm, wl_shm_pool, wl_surface, + }, +}; + +const W: i32 = 100; +const H: i32 = 100; +const STRIDE: i32 = W * 4; +const POOL_SIZE: i32 = STRIDE * H; +const SOCKET_NAME: &str = "wayland-test-06"; + +fn expected_pixel(x: i32, y: i32) -> u32 { + let a: u32 = 0xFF; + let r: u32 = (x as u32) & 0xFF; + let g: u32 = (y as u32) & 0xFF; + let b: u32 = ((x + y) as u32) & 0xFF; + (a << 24) | (r << 16) | (g << 8) | b +} + +fn errno_str() -> String { + let e = io::Error::last_os_error(); + format!("{e} (errno={})", e.raw_os_error().unwrap_or(0)) +} + +// ===================================================================== +// Server side +// ===================================================================== + +#[derive(Default)] +struct ServerObservation { + surface_created: bool, + pool_created_with_fd: Option, + buffer_created: Option<(i32, i32)>, // (w, h) + surface_attached: bool, + surface_committed: bool, + /// Set by main loop after reading pixels successfully + pixels_verified: bool, +} + +struct ServerState { + obs: Arc>, +} + +#[derive(Debug)] +struct DumbClientData; +impl ClientData for DumbClientData {} + +// ---- wl_compositor ---- +impl GlobalDispatch for ServerState { + fn bind( + _state: &mut Self, + _handle: &DisplayHandle, + _client: &Client, + resource: wayland_server::New, + _data: &(), + data_init: &mut DataInit<'_, Self>, + ) { + data_init.init(resource, ()); + } +} + +impl wayland_server::Dispatch for ServerState { + fn request( + state: &mut Self, + _client: &Client, + _resource: &wl_compositor::WlCompositor, + request: wl_compositor::Request, + _data: &(), + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, Self>, + ) { + if let wl_compositor::Request::CreateSurface { id } = request { + data_init.init(id, ()); + state.obs.lock().unwrap().surface_created = true; + eprintln!("[server] wl_compositor.create_surface"); + } + } +} + +// ---- wl_shm ---- +impl GlobalDispatch for ServerState { + fn bind( + _state: &mut Self, + _handle: &DisplayHandle, + _client: &Client, + resource: wayland_server::New, + _data: &(), + data_init: &mut DataInit<'_, Self>, + ) { + let shm = data_init.init(resource, ()); + // Advertise the format + shm.format(wl_shm::Format::Argb8888); + } +} + +impl wayland_server::Dispatch for ServerState { + fn request( + state: &mut Self, + _client: &Client, + _resource: &wl_shm::WlShm, + request: wl_shm::Request, + _data: &(), + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, Self>, + ) { + if let wl_shm::Request::CreatePool { id, fd, size } = request { + let raw_fd = fd.as_raw_fd(); + // Dup the fd so we keep it after the OwnedFd in the request is dropped + let our_fd = unsafe { libc::dup(raw_fd) }; + state.obs.lock().unwrap().pool_created_with_fd = Some(our_fd); + data_init.init(id, PoolData { fd: our_fd, size }); + eprintln!("[server] wl_shm.create_pool fd={raw_fd} size={size} (server dup={our_fd})"); + } + } +} + +// ---- wl_shm_pool ---- +struct PoolData { + fd: i32, + size: i32, +} + +impl wayland_server::Dispatch for ServerState { + fn request( + state: &mut Self, + _client: &Client, + _resource: &wl_shm_pool::WlShmPool, + request: wl_shm_pool::Request, + _data: &PoolData, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, Self>, + ) { + if let wl_shm_pool::Request::CreateBuffer { + id, + offset, + width, + height, + stride, + format, + } = request + { + data_init.init(id, ()); + state.obs.lock().unwrap().buffer_created = Some((width, height)); + eprintln!( + "[server] wl_shm_pool.create_buffer offset={offset} {width}x{height} stride={stride} format={format:?}" + ); + } + } +} + +// ---- wl_buffer ---- +impl wayland_server::Dispatch for ServerState { + fn request( + _state: &mut Self, + _client: &Client, + _resource: &wl_buffer::WlBuffer, + _request: wl_buffer::Request, + _data: &(), + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, Self>, + ) { + } +} + +// ---- wl_surface ---- +impl wayland_server::Dispatch for ServerState { + fn request( + state: &mut Self, + _client: &Client, + _resource: &wl_surface::WlSurface, + request: wl_surface::Request, + _data: &(), + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, Self>, + ) { + match request { + wl_surface::Request::Attach { .. } => { + state.obs.lock().unwrap().surface_attached = true; + eprintln!("[server] wl_surface.attach"); + } + wl_surface::Request::Damage { .. } + | wl_surface::Request::DamageBuffer { .. } => { + eprintln!("[server] wl_surface.damage"); + } + wl_surface::Request::Commit => { + state.obs.lock().unwrap().surface_committed = true; + eprintln!("[server] wl_surface.commit"); + } + _ => {} + } + } +} + +impl wayland_server::Dispatch for ServerState { + fn request( + _state: &mut Self, + _client: &Client, + _resource: &wl_callback::WlCallback, + _request: wl_callback::Request, + _data: &(), + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, Self>, + ) { + } +} + +// ===================================================================== +// Client side +// ===================================================================== + +#[derive(Default)] +struct ClientState { + compositor: Option, + shm: Option, +} + +impl ClientDispatch for ClientState { + fn event( + state: &mut Self, + registry: &wl_registry::WlRegistry, + event: wl_registry::Event, + _data: &(), + _conn: &ClientConnection, + qh: &ClientQueueHandle, + ) { + if let wl_registry::Event::Global { name, interface, version } = event { + eprintln!("[client] global #{name}: {interface} v{version}"); + match interface.as_str() { + "wl_compositor" => { + state.compositor = Some(registry.bind(name, version, qh, ())); + } + "wl_shm" => { + state.shm = Some(registry.bind(name, version, qh, ())); + } + _ => {} + } + } + } +} + +macro_rules! noop_dispatch { + ($iface:ty) => { + impl ClientDispatch<$iface, ()> for ClientState { + fn event( + _state: &mut Self, + _r: &$iface, + _event: <$iface as Proxy>::Event, + _data: &(), + _conn: &ClientConnection, + _qh: &ClientQueueHandle, + ) { + } + } + }; +} + +use wayland_client::Proxy; + +noop_dispatch!(ClientCompositor); +noop_dispatch!(ClientShm); +noop_dispatch!(ClientShmPool); +noop_dispatch!(ClientBuffer); +noop_dispatch!(ClientSurface); + +// ===================================================================== +// Process orchestration +// ===================================================================== + +unsafe fn create_shm_buffer(name: &str, size: usize) -> Result<(OwnedFd, *mut u8), String> { + let cname = CString::new(name).unwrap(); + let _ = libc::shm_unlink(cname.as_ptr()); + let fd = libc::shm_open(cname.as_ptr(), libc::O_RDWR | libc::O_CREAT, 0o600); + if fd < 0 { + return Err(format!("shm_open: {}", errno_str())); + } + if libc::ftruncate(fd, size as _) != 0 { + libc::close(fd); + return Err(format!("ftruncate: {}", errno_str())); + } + let p = libc::mmap( + ptr::null_mut(), + size, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_SHARED, + fd, + 0, + ); + if p == libc::MAP_FAILED { + libc::close(fd); + return Err(format!("mmap: {}", errno_str())); + } + Ok((unsafe { OwnedFd::from_raw_fd(fd) }, p as *mut u8)) +} + +use std::os::fd::FromRawFd; + +unsafe fn child_main(server_path: &str) -> ! { + eprintln!("[client pid={}] connecting to {server_path}", libc::getpid()); + + // Connect to the server socket + let stream = match UnixStream::connect(server_path) { + Ok(s) => s, + Err(e) => { + eprintln!("[client] connect: {e}"); + libc::_exit(2); + } + }; + let fd = stream.into_raw_fd(); + // wayland-client connect_to_env reads WAYLAND_SOCKET + std::env::set_var("WAYLAND_SOCKET", fd.to_string()); + + let conn = match ClientConnection::connect_to_env() { + Ok(c) => c, + Err(e) => { + eprintln!("[client] connect_to_env: {e}"); + libc::_exit(3); + } + }; + let mut event_queue: EventQueue = conn.new_event_queue(); + let qh = event_queue.handle(); + let _registry = conn.display().get_registry(&qh, ()); + + let mut state = ClientState::default(); + // Roundtrip to receive globals + if let Err(e) = event_queue.roundtrip(&mut state) { + eprintln!("[client] roundtrip 1: {e}"); + libc::_exit(4); + } + if state.compositor.is_none() || state.shm.is_none() { + eprintln!("[client] missing compositor/shm globals"); + libc::_exit(5); + } + eprintln!("[client] got compositor + shm globals"); + + // Create the shm buffer + let (shm_fd, map) = match create_shm_buffer("/wl-test-06-buffer", POOL_SIZE as usize) { + Ok(v) => v, + Err(e) => { + eprintln!("[client] shm: {e}"); + libc::_exit(6); + } + }; + + // Paint pattern + let pixels = std::slice::from_raw_parts_mut(map as *mut u32, (W * H) as usize); + for y in 0..H { + for x in 0..W { + pixels[(y * W + x) as usize] = expected_pixel(x, y); + } + } + eprintln!("[client] painted {}x{} ARGB pattern in shm", W, H); + + // Create wl_shm_pool from the fd + let shm = state.shm.clone().unwrap(); + let pool = shm.create_pool(shm_fd.as_fd(), POOL_SIZE, &qh, ()); + + // Create wl_buffer + let buffer = pool.create_buffer( + 0, W, H, STRIDE, + wayland_client::protocol::wl_shm::Format::Argb8888, + &qh, (), + ); + + // Create wl_surface + let compositor = state.compositor.clone().unwrap(); + let surface = compositor.create_surface(&qh, ()); + + // attach, damage, commit + surface.attach(Some(&buffer), 0, 0); + surface.damage_buffer(0, 0, W, H); + surface.commit(); + + eprintln!("[client] attached + damaged + committed"); + + // Flush all pending writes + if let Err(e) = event_queue.flush() { + eprintln!("[client] flush: {e}"); + libc::_exit(7); + } + // Roundtrip to ensure server has processed everything + let _ = event_queue.roundtrip(&mut state); + + eprintln!("[client] done, exit 0"); + libc::_exit(0); +} + +fn run() -> Result<(), String> { + println!("[test-06] Wayland shm pipeline on Redox"); + + // Pick a socket path. We won't use bind_auto because XDG_RUNTIME_DIR + // is not guaranteed inside the redoxer mini-VM. + let socket_path = format!("/tmp/{SOCKET_NAME}.sock"); + let _ = std::fs::remove_file(&socket_path); + let _ = std::fs::remove_file(format!("/tmp/{SOCKET_NAME}.sock.lock")); + + let listener = UnixListener::bind(&socket_path) + .map_err(|e| format!("UnixListener::bind({socket_path}): {e}"))?; + listener + .set_nonblocking(true) + .map_err(|e| format!("set_nonblocking: {e}"))?; + println!("[server] listening on {socket_path}"); + + let obs = Arc::new(Mutex::new(ServerObservation::default())); + + let mut display: ServerDisplay = ServerDisplay::new() + .map_err(|e| format!("Display::new: {e:?}"))?; + let mut dh = display.handle(); + dh.create_global::(5, ()); + dh.create_global::(1, ()); + + // Fork the client + let pid = unsafe { libc::fork() }; + if pid < 0 { + return Err(format!("fork: {}", errno_str())); + } + if pid == 0 { + unsafe { child_main(&socket_path) }; + } + + let mut state = ServerState { obs: obs.clone() }; + let start = Instant::now(); + let timeout = Duration::from_secs(5); + + let mut accepted_pid: Option = None; + + while start.elapsed() < timeout { + // Accept new client + if accepted_pid.is_none() { + match listener.accept() { + Ok((stream, _addr)) => { + stream.set_nonblocking(true).ok(); + dh.insert_client(stream, Arc::new(DumbClientData)) + .map_err(|e| format!("insert_client: {e:?}"))?; + accepted_pid = Some(pid); + println!("[server] accepted client connection"); + } + Err(e) if e.kind() == io::ErrorKind::WouldBlock => {} + Err(e) => return Err(format!("accept: {e}")), + } + } + + display + .dispatch_clients(&mut state) + .map_err(|e| format!("dispatch_clients: {e:?}"))?; + display + .flush_clients() + .map_err(|e| format!("flush_clients: {e:?}"))?; + + let snap = obs.lock().unwrap(); + if snap.surface_committed && snap.pool_created_with_fd.is_some() { + // We have all we need to verify pixels + break; + } + drop(snap); + + std::thread::sleep(Duration::from_millis(10)); + } + + // Capture observation state + let surface_committed; + let buffer_created; + let surface_attached; + let pool_fd; + { + let s = obs.lock().unwrap(); + surface_committed = s.surface_committed; + buffer_created = s.buffer_created; + surface_attached = s.surface_attached; + pool_fd = s.pool_created_with_fd; + } + + if !surface_committed { + return Err("server did not observe wl_surface.commit within 5s".into()); + } + if buffer_created.is_none() { + return Err("server did not observe wl_shm_pool.create_buffer".into()); + } + if !surface_attached { + return Err("server did not observe wl_surface.attach".into()); + } + let pool_fd = pool_fd.ok_or("server did not receive shm fd")?; + + println!("[server] all expected protocol events observed; mapping fd to verify pixels"); + + // Map the received fd and verify pixels + let map = unsafe { + libc::mmap( + ptr::null_mut(), + POOL_SIZE as usize, + libc::PROT_READ, + libc::MAP_SHARED, + pool_fd, + 0, + ) + }; + if map == libc::MAP_FAILED { + return Err(format!("server mmap: {}", errno_str())); + } + let pixels = unsafe { std::slice::from_raw_parts(map as *const u32, (W * H) as usize) }; + let mut mismatches: Vec<(i32, i32, u32, u32)> = Vec::new(); + for y in 0..H { + for x in 0..W { + let got = pixels[(y * W + x) as usize]; + let want = expected_pixel(x, y); + if got != want && mismatches.len() < 5 { + mismatches.push((x, y, got, want)); + } + } + } + unsafe { libc::munmap(map, POOL_SIZE as usize) }; + if !mismatches.is_empty() { + for (x, y, got, want) in &mismatches { + eprintln!("[server] mismatch at ({x},{y}): got {got:#010x} want {want:#010x}"); + } + return Err(format!("{} pixel mismatches in {} total", mismatches.len(), W * H)); + } + + println!( + "[server] all {} pixels match expected pattern (read via fd received through Wayland protocol)", + W * H + ); + + // Reap child + let mut status: libc::c_int = 0; + unsafe { libc::waitpid(pid, &mut status, 0) }; + let exit_code = unsafe { libc::WEXITSTATUS(status) }; + if exit_code != 0 { + return Err(format!("child exited with status {exit_code}")); + } + println!("[server] child reaped cleanly"); + + // Cleanup + let _ = std::fs::remove_file(&socket_path); + let _ = std::fs::remove_file(format!("{socket_path}.lock")); + let cname = CString::new("/wl-test-06-buffer").unwrap(); + unsafe { libc::shm_unlink(cname.as_ptr()) }; + + Ok(()) +} + +fn main() -> ExitCode { + match run() { + Ok(()) => { + println!("[test-06] PASS: full Wayland shm pipeline works on Redox"); + ExitCode::SUCCESS + } + Err(e) => { + eprintln!("[test-06] FAIL: {e}"); + ExitCode::FAILURE + } + } +} diff --git a/crates/redox-wl-test-unix-socket/Cargo.toml b/crates/redox-wl-test-unix-socket/Cargo.toml new file mode 100644 index 0000000..791c5bf --- /dev/null +++ b/crates/redox-wl-test-unix-socket/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "redox-wl-test-unix-socket" +version = "0.1.0" +edition = "2021" + +[dependencies] +libc = "0.2" diff --git a/crates/redox-wl-test-unix-socket/src/main.rs b/crates/redox-wl-test-unix-socket/src/main.rs new file mode 100644 index 0000000..3c8164e --- /dev/null +++ b/crates/redox-wl-test-unix-socket/src/main.rs @@ -0,0 +1,78 @@ +//! Test 1 — Unix socket SOCK_STREAM with Wayland-shaped messages. +//! +//! Wayland wire format (libwayland reference): +//! - Header: object_id (u32) + (size << 16 | opcode) (u32) = 8 bytes +//! - Payload: variable, 4-byte aligned +//! +//! This test: +//! 1. Creates a SOCK_STREAM socketpair (AF_UNIX) +//! 2. Side A sends a message with header + payload +//! 3. Side B reads it back, validates byte-for-byte +//! 4. Ensures partial reads don't corrupt the stream + +use std::io::{Read, Write}; +use std::os::unix::net::UnixStream; +use std::process::ExitCode; + +const OBJECT_ID: u32 = 0x0000_0001; +const OPCODE: u16 = 0x000A; +const PAYLOAD: &[u8] = b"hello-wayland-on-redox-roundtrip"; + +fn main() -> ExitCode { + println!("[test-01] Unix socket SOCK_STREAM Wayland-shaped roundtrip"); + + let (mut a, mut b) = match UnixStream::pair() { + Ok(pair) => pair, + Err(e) => { + eprintln!("[test-01] FAIL: UnixStream::pair: {e}"); + return ExitCode::FAILURE; + } + }; + + let payload_len = PAYLOAD.len(); + let aligned_len = (payload_len + 3) & !3; + let total_size = 8 + aligned_len; + + let mut msg = Vec::with_capacity(total_size); + msg.extend_from_slice(&OBJECT_ID.to_ne_bytes()); + let size_op: u32 = ((total_size as u32) << 16) | (OPCODE as u32); + msg.extend_from_slice(&size_op.to_ne_bytes()); + msg.extend_from_slice(PAYLOAD); + msg.resize(total_size, 0); + + if let Err(e) = a.write_all(&msg) { + eprintln!("[test-01] FAIL: write_all: {e}"); + return ExitCode::FAILURE; + } + println!("[test-01] sent {} bytes (header 8 + payload {} aligned to {})", + total_size, payload_len, aligned_len); + + let mut header = [0u8; 8]; + if let Err(e) = b.read_exact(&mut header) { + eprintln!("[test-01] FAIL: read header: {e}"); + return ExitCode::FAILURE; + } + let recv_oid = u32::from_ne_bytes(header[0..4].try_into().unwrap()); + let recv_so = u32::from_ne_bytes(header[4..8].try_into().unwrap()); + let recv_size = (recv_so >> 16) as usize; + let recv_op = (recv_so & 0xFFFF) as u16; + + if recv_oid != OBJECT_ID || recv_op != OPCODE || recv_size != total_size { + eprintln!("[test-01] FAIL: header mismatch oid={recv_oid:#x} op={recv_op:#x} size={recv_size}"); + return ExitCode::FAILURE; + } + + let mut buf = vec![0u8; aligned_len]; + if let Err(e) = b.read_exact(&mut buf) { + eprintln!("[test-01] FAIL: read payload: {e}"); + return ExitCode::FAILURE; + } + if &buf[..payload_len] != PAYLOAD { + eprintln!("[test-01] FAIL: payload mismatch"); + return ExitCode::FAILURE; + } + + println!("[test-01] PASS: roundtrip OK, {} bytes recv, oid={:#x} op={:#x}", + total_size, recv_oid, recv_op); + ExitCode::SUCCESS +} diff --git a/docs/existing-redox-gui.md b/docs/existing-redox-gui.md new file mode 100644 index 0000000..33fcfae --- /dev/null +++ b/docs/existing-redox-gui.md @@ -0,0 +1,486 @@ +# Audit Phase 1 — Existant Redox GUI + +> Document produit le 2026-05-08 dans le cadre du plan directeur +> `REDOX_COSMIC_XWAYLAND_RS_PLAN.md` (phase 1 : audit avant code). +> +> **Périmètre** : comprendre l'architecture graphique actuelle de Redox +> (Orbital + drivers + primitives) pour calibrer le portage Wayland. + +## Résumé exécutif + +Redox dispose **déjà d'une fondation graphique plus mûre que prévu** : + +- **Une API DRM compatible Linux** (subset KMS) exposée via `graphics-ipc` et utilisée par Orbital +- **Plusieurs drivers** : VESA firmware (`vesad`), virtio-gpu (`virtio-gpud`), Intel HD (`ihdgd`) +- **Un input multiplexer** (`inputd`) qui gère VT switching, keymaps, producteurs/consommateurs +- **Orbital** très compact (~3900 lignes Rust) qui valide le pattern serveur-clients via scheme + +Côté primitives système, **l'essentiel de ce que Wayland exige est déjà là** : + +- AF_UNIX sockets via le scheme `chan:` ✅ +- `sendmsg`/`recvmsg` avec **SCM_RIGHTS implémenté** (fd passing) ✅ +- `shm_open` via `/scheme/shm/` ✅ +- `mmap MAP_SHARED` ✅ +- `poll`/`epoll` ✅ +- ⚠️ `memfd_create` : numéro syscall défini mais implémentation à vérifier + +**Conclusion** : la phase 2 (primitives OS) du plan directeur sera moins lourde que craint. +La majorité des briques existent — il faudra surtout valider le comportement par tests dédiés. + +--- + +## 1. Architecture Orbital + +### 1.1 Point d'entrée et boot + +`orbital/src/main.rs` (82 lignes) : + +```text +main() + └─ orbital() + ├─ lit env "VT" (passé par inputd au boot) + ├─ Orbital::open_display() ← ouvre display via inputd + ├─ exec "inputd -A " ← s'enregistre comme handler du VT + ├─ Config::from_path("/ui/orbital.toml") + ├─ OrbitalScheme::new(displays, config) + └─ orbital.run(scheme, login_cmd) ← event loop principal +``` + +**Note séquence de boot** : c'est `inputd` qui orchestre l'ouverture du display +au moment où le VT s'active. Orbital ne touche pas directement aux drivers +graphiques — il passe par inputd qui lui retourne un fd `/scheme/.../v2/...`. + +### 1.2 Event loop (`core/mod.rs`, 662 lignes) + +Architecture deux-fd : + +```text +EventQueue + ├─ Scheme fd (clients Orbital) → SchemeSync trait + └─ Input fd (depuis inputd consumer) → ConsumerHandleEvent::Events +``` + +**Patterns clés** : + +- Utilise `event::EventQueue` (crate `redox_event`) — équivalent Redox de + `wl_event_loop` Wayland ou `epoll` Linux +- Chaque source a son fd, multiplexé en édge-triggered READ +- Les requests scheme bloquantes sont stockées dans `delayed: VecDeque` puis + réveillées quand un input arrive (équivalent du `wl_display_dispatch_pending` + Wayland) + +**Réutilisable pour le compositor Wayland** : + +- Le pattern EventQueue avec sources typées (`user_data!` macro) est exactement + ce qu'il faut pour le compositor Wayland +- Le mécanisme `delayed` pour les reads bloquants est réutilisable tel quel + +### 1.3 Protocole `orbital:` (le scheme) + +Implémenté dans `core/mod.rs::OrbitalHandler::SchemeSync` et `scheme.rs` (1721 lignes). + +**API client** (ce qu'un client appelle pour ouvrir une fenêtre) : + +```text +open("/scheme/orbital//////") + → returns window_fd + +write(window_fd, "T,nouveau titre") → titre +write(window_fd, "S,800,600") → resize +write(window_fd, "P,100,200") → position +write(window_fd, "Y,0,0,800,600,...") → damage rectangles +write(window_fd, "F,t,1") → flag transparent on +read(window_fd, &mut events) → pull events (clavier, souris, ...) +mmap_prep(window_fd, ...) → obtient un buffer mmap zero-copy +fsync(window_fd) → commit (équivalent wl_surface.commit) +``` + +**Concepts à transposer en Wayland** : + +| Orbital | Équivalent Wayland | +|---|---| +| open() avec path verbeux | `xdg_surface` + `xdg_toplevel` configure | +| write("S,...") resize | `xdg_toplevel.configure` + ack | +| write("Y,...") damage | `wl_surface.damage_buffer` | +| mmap_prep | `wl_shm_pool.create_buffer` + `wl_surface.attach` | +| fsync | `wl_surface.commit` | +| read events | event queue Wayland | + +**Limites du protocole Orbital** : + +- API basée sur strings dans les writes — pas type-safe, pas de versioning +- Une seule sémantique d'extension (préfixes "A","D","F","M","P","S","T","Y") +- Pas de négociation de capabilities côté serveur +- Buffer mmap'é direct — moins flexible que `wl_shm_pool` mais plus rapide + +### 1.4 Composition (`compositor.rs`, 281 lignes) + +Compositor logiciel CPU pur : + +- `Image::roi_mut()` pour écrire dans le framebuffer +- `Color` ARGB8888 +- Damage rectangles propagés aux clients via `Y,...` +- Curseur SW si pas de hardware cursor disponible (cf `core/display.rs:140` + `set_client_capability(CursorPlaneHotspot, true)`) + +**Réutilisable directement** : la logique damage tracking + composition CPU +est exactement ce qu'il faut pour la phase 6-7 du plan (`wl_shm` + composition). + +--- + +## 2. Stack graphique Redox + +### 2.1 API `graphics-ipc` (la lib commune) + +`base/drivers/graphics/graphics-ipc/src/lib.rs` (127 lignes). + +**Découverte critique** (commentaire ligne 13-15) : + +> *The v2 graphics API allows creating framebuffers on the fly, using them for +> page flipping and handles all displays using a single fd. This is basically +> a subset of the Linux DRM interface with a couple of custom ioctls in the +> place of the KMS ioctls that are missing.* + +**Conséquence** : Redox a une **API DRM compatible Linux** (subset KMS). +La crate Rust `drm` upstream fonctionne directement sur Redox. + +API exposée : + +```rust +V2GraphicsHandle::from_file(file) + ├─ resource_handles() ← liste connectors/encoders/CRTCs + ├─ get_connector(handle, force_probe) + ├─ get_encoder(handle) + ├─ create_dumb_buffer(size, fourcc, bpp) + ├─ map_dumb_buffer(buffer) ← mmap CPU-accessible + ├─ add_framebuffer(buffer, depth, bpp) + ├─ set_crtc(crtc, fb, position, connectors, mode) + ├─ dirty_framebuffer(fb, &[ClipRect]) ← damage flush + ├─ move_cursor / set_cursor2 + └─ destroy_* +``` + +`CpuBackedBuffer` (lignes 46-127) : +- Wrapper sur `DumbBuffer` DRM +- Optionnel : shadow buffer host (write-combining mitigation) +- `sync_rect()` pour copier shadow → on-screen par damage + +**Implications pour le portage Wayland** : + +1. Pas besoin de réinventer le display backend — l'API DRM existe +2. Peut être utilisée telle quelle par un compositor Wayland Rust +3. `CpuBackedBuffer::shadow_buf()` retourne `&mut [u8]` — exactement + le format SHM que Wayland attend pour la composition + +### 2.2 Drivers graphiques disponibles + +Localisation : `base/drivers/graphics/`. + +| Driver | Cible | Statut | Notes | +|---|---|---|---| +| **`vesad`** (133 + 275 lignes) | VESA framebuffer firmware (BIOS/UEFI) | Stable | Lit FRAMEBUFFER_* env, support multi-FB | +| **`virtio-gpud`** (615 + 528 lignes) | virtio-gpu (QEMU/KVM) | Actif | C'est ce qui tourne sur ton image desktop | +| **`ihdgd`** | Intel HD Graphics | Actif | Driver natif HW | +| `fbcond` | Console framebuffer | — | Texte console | +| `fbbootlogd` | Boot log framebuffer | — | Affichage early boot | +| `console-draw` | Helpers dessin console | — | Lib commune | +| `driver-graphics` | Couche d'abstraction commune (`GraphicsScheme`) | — | C'est elle qui exporte `display.<driver>` | + +Tous publient leur scheme sous forme `display.<nom>` (ex: `display.vesa`). + +### 2.3 Chemin d'ouverture du display + +```text +Orbital + └─> ConsumerHandle::new_vt() // /scheme/input/consumer + └─> open_display_v2() + └─> fpath() pour récupérer le path complet + └─> open("/scheme/.../v2/<display>") // fd graphics-ipc v2 + └─> V2GraphicsHandle::from_file +``` + +C'est `inputd` qui détient la connaissance du driver graphique actif et +expose un chemin standard. Orbital ne hard-code pas `vesa` ou `virtio-gpu` — +il demande à inputd. + +**Implication pour le compositor Wayland** : suivre le même pattern. +Demander à `inputd` quel display est associé au VT, ouvrir le fd retourné, +le wrapper en `V2GraphicsHandle`. Pas de hardcode de driver. + +### 2.4 `vesad` — le chemin firmware framebuffer + +`base/drivers/graphics/vesad/src/main.rs` (133 lignes). + +**Bootstrap** (lignes 19-44) : +```rust +FRAMEBUFFER_WIDTH // hex string, ex "780" → 1920 +FRAMEBUFFER_HEIGHT // ex "438" → 1080 +FRAMEBUFFER_ADDR // adresse physique du FB fournie par firmware +FRAMEBUFFER_STRIDE // bytes par ligne +``` + +Le bootloader (UEFI/BIOS) place ces variables avant de chainer vers le kernel. +`vesad` mappe l'adresse physique en mémoire userspace et expose le FB via +`GraphicsScheme`. + +**Fallback bootloader env** (lignes 60-84) : `vesad` lit `/scheme/sys/env` +pour `FRAMEBUFFER1`, `FRAMEBUFFER2`, ... — support multi-écran natif au boot +si le firmware en remonte plusieurs. + +**Implication** : pour la phase 4 du plan (backend display), partir de `vesad` +comme chemin de validation initial = sûr et simple. virtio-gpud est plus +puissant mais plus complexe (gpu commandes, virgl optionnel). + +--- + +## 3. Stack input + +### 3.1 `inputd` — le multiplexer + +`base/drivers/inputd/src/{lib.rs,main.rs}` (211 + 663 lignes). + +**Schemes exposés** : + +| Path | Pour | Rôle | +|---|---|---| +| `/scheme/input/consumer` | Orbital, futur compositor | reçoit les events triés (orbclient::Event) | +| `/scheme/input/consumer_bootlog` | bootlog | events early-boot | +| `/scheme/input/producer` | drivers ps2d, usbhidd | écrit des events bruts | +| `/scheme/input/control` | gestion VT/keymap | active VT, change keymap | +| `/scheme/input/handle/<scheme>` | délégation à un autre scheme | déléguer un VT à un autre serveur | +| `/scheme/input/handle_early/<scheme>` | idem mais avant init VT | bootlog, etc. | + +**API ConsumerHandle** : + +```rust +let h = ConsumerHandle::new_vt()?; +let display_fd = h.open_display_v2()?; // ← ouvre le display de ce VT +let evt_fd = h.event_handle(); // ← fd à mettre dans EventQueue +match h.read_events(&mut events)? { + ConsumerHandleEvent::Events(slice) => /* traite events */, + ConsumerHandleEvent::Handoff => /* VT change, on perd le contrôle */, +} +``` + +Le **handoff via `ESTALE`** est crucial : quand le VT switch (Ctrl-Alt-F2), +inputd retourne ESTALE — Orbital sait qu'il doit se mettre en veille +proprement. Le compositor Wayland devra gérer la même sémantique. + +### 3.2 Drivers input + +`base/drivers/input/` : +- `ps2d` — clavier/souris PS/2 (legacy + KVM) +- `usbhidd` — HID USB (clavier, souris, tablette) + +Tous écrivent vers `/scheme/input/producer`. inputd dédoublonne, séquence, +attribue au VT actif, transmet au consumer. + +### 3.3 Format d'event + +`orbclient::Event` (struct C-compatible) — défini dans la crate `orbclient`. +Types : `KeyEvent`, `MouseEvent`, `MouseRelativeEvent`, `ButtonEvent`, +`ScrollEvent`, `FocusEvent`, `QuitEvent`, etc. + +**Pour Wayland** : +- Mapping `orbclient::KeyEvent` → `wl_keyboard.key` : direct +- Codes de touches : `orbclient` utilise des scancodes — il faudra + traduire vers les **keycodes XKB** que Wayland attend +- ⚠️ **xkb keymap** : à concevoir, voir section 5 + +--- + +## 4. Primitives OS pour Wayland (relibc) + +Vérifications effectuées dans `~/Projets/Redox/relibc/src/`. + +### 4.1 Sockets Unix (AF_UNIX) + +`platform/redox/socket.rs` : + +- AF_UNIX **bind/connect** : implémenté via syscall direct (commentaire ligne 69 : + *"bind/connect with AF_UNIX were replaced with SYS_CALL"*) +- Path encoding : `chan:<path>` (ligne 177 : `buf.starts_with(b"chan:")`) +- SOCK_STREAM ✅ et SOCK_DGRAM ✅ supportés +- Compatible avec l'usage Wayland (`$XDG_RUNTIME_DIR/wayland-N`) + +### 4.2 fd passing — `sendmsg` / `recvmsg` avec SCM_RIGHTS + +**🟢 Critique** : `platform/redox/socket.rs` : +- Lignes 272 et 447 : `(SOL_SOCKET, SCM_RIGHTS)` traité dans cmsghdr +- Lignes 835 et 924 : `recvmsg` / `sendmsg` implémentés en interne + +C'est **le pré-requis n°1** du plan (risque majeur 1) — il est rempli. +Il faut quand même écrire des tests dédiés en phase 2 pour valider : +- Passage simple de fd entre deux processus +- Multiple fds dans un même cmsg +- Passage d'un fd shm + lecture côté receveur + +### 4.3 Mémoire partagée — `shm_open` + mmap + +**`shm_open`** : `header/sys_mman/mod.rs:185` : +```rust +pub unsafe extern "C" fn shm_open(name, oflag, mode) -> c_int { + // traduit "/foo" → "/scheme/shm/foo" puis open() classique +} +``` + +Mécanisme : un scheme dédié `/scheme/shm/` sert de namespace +(équivalent `/dev/shm` Linux mais via le scheme system). + +**`mmap MAP_SHARED`** : présent (`header/sys_mman/mod.rs`). Tous les buffers +DRM dumb sont déjà mappés ainsi par `graphics-ipc`. + +### 4.4 ⚠️ `memfd_create` + +- Numéro syscall défini : `__NR_memfd_create = 319` (x86_64) +- **Implémentation** : pas vue dans la sortie grep — probablement absente + ou stub +- **Impact pour Wayland** : libwayland-client moderne préfère `memfd_create` + pour les shm pools (pas de path needed). Fallback : `shm_open` avec nom + unique → fonctionne, juste un peu plus verbeux + +À traiter en phase 2 : +- Soit implémenter `memfd_create` au-dessus de `shm_open + shm_unlink` +- Soit ajouter une couche `cfg(target_os = "redox")` côté `wayland-client` + qui force le path shm_open + +### 4.5 Event loop / poll + +- `header/poll/mod.rs` : `poll()` POSIX disponible +- `header/sys_select/mod.rs` : `select` POSIX +- `header/sys_epoll/mod.rs` : `epoll_*` (probablement émulé sur Redox via + l'EventQueue native — à vérifier) +- **Native Redox** : `redox_event::EventQueue` — utilisé par Orbital, plus + idiomatique. Pour le compositor Rust pur, autant l'utiliser directement + plutôt que de passer par poll() POSIX. + +--- + +## 5. Manques identifiés (à traiter dans le plan) + +### 5.1 Pas de keymap XKB sur Redox + +Wayland exige que le serveur envoie un keymap XKB au client (`wl_keyboard.keymap` +event, format `XKB_V1`). Or : +- Redox utilise des scancodes orbclient +- Pas de `xkbcommon` packagé sur Redox (à confirmer en cherchant la recette) +- Pas de fichiers de keymap XKB système + +**À traiter en phase 5 (backend input)** : +- Option A : porter `libxkbcommon` (C, ~10k loc) +- Option B : utiliser la crate Rust `xkb` (wrapper xkbcommon, dépend du C) +- Option C : implémenter un sous-ensemble XKB en Rust (plus long mais + cohérent avec l'esprit du projet) + +Recommandation : **Option C** mais avec scope minimal (US layout + variantes +basiques) au début. La crate `xkeysym` peut servir de base. + +### 5.2 Pas d'AT-SPI / dbus pour l'accessibilité + +- Phase 11 du plan vise navigation clavier + métadata d'accessibilité +- Sur Linux, AT-SPI transporte ces métadata via dbus +- Dbus existe sur Redox mais portage limité ; AT-SPI n'est pas porté +- À concevoir : protocole Redox simple d'accessibilité, ou wayland-protocols + custom (text-input-v3 comme point de départ) + +### 5.3 GPU userspace driver manquant + +- vesad et virtio-gpud existent mais pas de DRM driver "GPU complet" hors virtio +- Mesa3D Redox = LLVMpipe seulement (CPU) +- Phase 12 du plan = GPU expérimental — restera CPU pour le MVP +- À documenter : aucune accélération hardware n'est garantie pour les phases 0-11 + +### 5.4 Pas de session manager / VT API stable + +- Sur Linux : `logind` / `seatd` gèrent les VT et l'accès aux devices +- Sur Redox : `inputd` joue un rôle similaire mais pas équivalent +- Pour Smithay (phase 13), il faudra un backend session Redox basé sur inputd + +--- + +## 6. Ce qui est réutilisable directement + +| Composant Redox actuel | Usage dans le compositor Wayland | +|---|---| +| `redox_event::EventQueue` | Boucle principale du compositor | +| `redox-scheme::SchemeSync` | Si on choisit aussi de publier un scheme `wayland:` | +| `graphics-ipc::V2GraphicsHandle` | Backend display (phase 4) — direct | +| `graphics-ipc::CpuBackedBuffer` | wl_shm composition (phase 6) — direct | +| `inputd::ConsumerHandle` | Backend input (phase 5) — direct | +| `orbital/src/compositor.rs` damage tracking | Référence pour notre damage logic (phase 6-7) | +| `orbital/src/scheme.rs` focus + raccourcis | Référence pour focus management (phase 7) | +| `redox-log` | Logging | +| Crate `drm` upstream | Si on veut adresser DRM directement | + +--- + +## 7. Décisions recommandées pour la suite + +### Phase 2 (primitives OS) — peut être réduite + +Plutôt que d'auditer toutes les primitives, **se limiter à des tests dédiés** : + +1. Test fd passing : 2 processus, l'un crée un memfd-like, l'autre le mappe +2. Test Unix socket SOCK_STREAM avec messages Wayland-shaped (header binaire) +3. Test event loop multi-fd avec signaux POSIX +4. Test concurrent shm_open/mmap depuis plusieurs processus + +Si les 4 tests passent, on a la garantie que `wayland-rs` peut tourner. +Estimation : **1 mois à 2h/jour** au lieu des 6 mois initialement budgétés. + +### Phase 3 (port wayland-rs) — à découper + +`wayland-rs` upstream a peu de deps OS-specifiques dans son crate `wayland-backend`. +Ordre suggéré : +1. `wayland-scanner` (génération code XML → Rust) : pure logic, devrait marcher tel quel +2. `wayland-backend` : couche socket — vérifier le `cfg(unix)` vs ajout d'un + `cfg(target_os = "redox")` +3. `wayland-server` : haut niveau — devrait fonctionner si backend OK +4. `wayland-client` : symétrique + +### Phase 4 (display backend) — voie rapide possible + +`V2GraphicsHandle` est si proche de l'API DRM Linux qu'on peut s'inspirer +directement de `core/display.rs` d'Orbital pour le RedoxOutput. + +**Estimation : 2 mois à 2h/jour** (au lieu des 6 mois si on partait de zéro). + +--- + +## 8. Repos clonés localement pour la suite + +```text +~/Projets/Redox/ + ├── orbital/ ← référence compositor (gitlab.redox-os.org/redox-os/orbital) + ├── base/ ← drivers + graphics-ipc + inputd (gitlab.redox-os.org/redox-os/base) + ├── relibc/ ← libc Redox, primitives POSIX (--depth 1) + ├── redox-src/ ← repo umbrella, ~/Projets/Redox/redox-src/build/x86_64/desktop/harddrive.img + └── docs/existing-redox-gui.md ← ce document +``` + +Sources additionnelles à cloner si on creuse plus : +- `redox-os/wayland-rs-redox-fork` *(à créer le moment venu)* +- `redox-os/drivers` *(déjà inclus dans base/drivers/)* +- `Smithay/smithay` *(plus tard, phase 13)* + +--- + +## 9. Documents à produire ensuite + +Selon le plan directeur (section "Documents A Produire Ensuite") : + +| Document | Quand | Contenu | +|---|---|---| +| `redox-wayland-primitives.md` | Phase 2, après tests | Résultats des 4 tests dédiés + conclusions | +| `compositor-architecture.md` | Phase 3-4 | Crates, traits, flux de données | +| `display-backend-redox.md` | Phase 4 | Détails RedoxOutput + dirty_framebuffer flow | +| `input-backend-redox.md` | Phase 5 | Mapping orbclient → Wayland + XKB strategy | +| `software-rendering-and-gpu-roadmap.md` | Phase 4 + 12 | CPU paths + GPU horizon | +| `accessibility-roadmap.md` | Phase 11 | Décisions sans AT-SPI | +| `orbital-migration.md` | Phase 8-9 | Comment porter winit/softbuffer/clients | +| `cosmic-smithay-roadmap.md` | Phase 13 | Quand y aller, dépendances bloquantes | +| `x11-compat-rust-roadmap.md` | Phase 14 | Projet séparé | + +--- + +*Fin de l'audit phase 1.* diff --git a/docs/redox-wayland-primitives.md b/docs/redox-wayland-primitives.md new file mode 100644 index 0000000..64bd25a --- /dev/null +++ b/docs/redox-wayland-primitives.md @@ -0,0 +1,296 @@ +# Phase 2 — Primitives Redox pour Wayland : résultats des tests + +> Document produit le 2026-05-08 dans le cadre du plan directeur +> `REDOX_COSMIC_XWAYLAND_RS_PLAN.md`. +> +> **Périmètre** : valider par tests dédiés que les 4 primitives système +> indispensables à Wayland (Unix sockets, fd passing SCM_RIGHTS, shm_open+mmap, +> poll multi-fd) fonctionnent sur Redox. Décision conditionnée par l'audit +> phase 1 (`existing-redox-gui.md`) qui a montré que ces primitives +> existent en théorie dans relibc. + +## Verdict global + +**✅ Wayland est techniquement viable sur Redox.** + +Les 4 primitives critiques fonctionnent dans le scénario réel Wayland +(client + compositor en deux processus séparés). **Aucun blocage** identifié +pour démarrer la phase 3 (port `wayland-rs`). + +| Test | Scope | Résultat | +|---|---|---| +| Test 1 — Unix socket SOCK_STREAM | mono-process via socketpair | ✅ PASS | +| Test 2 — SCM_RIGHTS | mono-process | ⚠️ Artefact (numéro fd réutilisé) | +| Test 2b — SCM_RIGHTS | multi-process (fork) | ✅ PASS | +| Test 3 — shm_open + mmap MAP_SHARED | mono-process | ✅ PASS | +| Test 4 — poll() multi-fd | mono-process | ✅ PASS | + +## Méthodologie + +Workspace : `~/Projets/Redox/redox-wayland-tests/` +Toolchain : `nightly-2026-05-07` + target `x86_64-unknown-redox` +Exécution : `redoxer run --release` (mini-VM Redox sous QEMU/KVM) + +Chaque test est un binaire Rust autonome utilisant uniquement la crate +`libc 0.2` (pour rester proche du métal et éviter d'introduire des biais +d'abstraction). + +--- + +## Test 1 — Unix socket SOCK_STREAM avec format Wayland + +### Objectif +Valider qu'un message Wayland-shaped (header `object_id` + `(size << 16 | opcode)`, +payload aligné sur 4 bytes) traverse une socketpair AF_UNIX/SOCK_STREAM sans +corruption. + +### Setup +- `socketpair(AF_UNIX, SOCK_STREAM, 0, fds)` +- Construire un message : 8 bytes header + 32 bytes payload aligné = 40 bytes +- write_all côté A, read_exact header puis payload côté B + +### Résultat +```text +[test-01] sent 40 bytes (header 8 + payload 32 aligned to 32) +[test-01] PASS: roundtrip OK, 40 bytes recv, oid=0x1 op=0xa +``` + +**Conclusion** : `wayland-rs/wayland-backend` peut utiliser des Unix sockets +Redox sans modification au niveau wire format. + +--- + +## Test 2 — SCM_RIGHTS mono-process + +### Objectif +Valider l'API SCM_RIGHTS via `sendmsg`/`recvmsg`. Vérifier que le fd +reçu donne accès au même fichier que le fd envoyé. + +### Observation initiale (FAIL) +En mono-process : +- `sendmsg(SCM_RIGHTS, [fd=5])` → succès +- `recvmsg(...)` → control de 24 bytes, fd reçu = **5** (identique) +- `dup(tmp_fd) → 8`, send 8, recv → **8** + +Conclusion : le kernel Redox **réutilise le numéro fd** plutôt que de créer +un nouveau slot dans la fd table. Quand on `close(tmp_fd)` puis `close(dup)`, +le fd reçu devient EBADF. + +### Interprétation +Ce comportement est **un artefact mono-process** : +- Mono-process, le sender et le receiver partagent la même fd table +- Le kernel optimise en évitant de créer un slot redondant +- Ce n'est **pas** un comportement représentatif du cas Wayland réel + (client et compositor sont toujours en deux processus séparés) + +### Code relibc concerné +`relibc/src/platform/redox/socket.rs:289` (send) et `:467` (recv) : +```rust +redox_rt::sys::sys_call_wo(socket, &fds_slice, CallFlags::FD, &[])? +redox_rt::sys::sys_call_ro(socket, fds_bytes, call_flags, &[])? +``` +Le syscall Redox `sys_call_wo/ro` avec `CallFlags::FD` est l'implémentation +réelle. Sa sémantique multi-process est validée par le test 2b ci-dessous. + +--- + +## Test 2b — SCM_RIGHTS multi-process (le vrai test Wayland) + +### Objectif +Valider que SCM_RIGHTS transfère effectivement un fd d'un processus parent +vers un processus enfant créé par `fork()`. C'est le scénario Wayland exact. + +### Setup +1. Parent : `open("/tmp/...")` avec un marker connu +2. Parent : `socketpair(AF_UNIX, SOCK_STREAM)` +3. Parent : `fork()` +4. Parent : `sendmsg(SCM_RIGHTS, [tmp_fd])`, drop tmp_fd +5. Child : `recvmsg`, lit via le fd reçu, vérifie le marker, `_exit(0/N)` +6. Parent : `waitpid`, recupère exit code + +### Résultat +```text +[test-02b PARENT] tmp fd=5, sockets 6/7 +[test-02b PARENT] forked child pid=36, sending fd +[test-02b CHILD pid=36] recvmsg on sock 7 +[test-02b CHILD] received fd=5 +[test-02b CHILD] marker matches, exit 0 +[test-02b PARENT] child reaped: exited=true code=0 +[test-02b] PASS: fd passed across fork() correctly +``` + +**Conclusion critique** : Le child a reçu fd=5 (numéro réutilisé localement, +ce qui est normal puisque sa fd table est indépendante du parent), et la +**lecture via ce fd a retourné le marker écrit par le parent**. + +C'est la sémantique POSIX correcte pour SCM_RIGHTS et c'est **exactement +ce que Wayland exige** : un client envoie un fd shm au compositor, le +compositor reçoit dans sa propre fd table un descripteur fonctionnel. + +### Implication pour le projet + +Le risque majeur n°1 du plan directeur (cf section "Risques Majeurs") est levé : + +> *Wayland depend fortement de sockets, fd passing, shm et mmap. +> Mitigation : traiter cette phase avant tout port COSMIC ; +> maintenir des tests bas niveau.* + +Pré-requis fonctionnel : ✅ confirmé. +Tests bas niveau à maintenir : ✅ ce document + le code source +`redox-wayland-tests/`. + +--- + +## Test 3 — shm_open + mmap MAP_SHARED + +### Objectif +Valider que `shm_open` (qui sur Redox est traduit en +`open("/scheme/shm/<name>")` par relibc) supporte le pattern Wayland : +créer une zone partagée nommée, écrire, refermer, rouvrir, relire. + +### Setup +1. `shm_open("/redox-wl-test-03", O_RDWR|O_CREAT, 0600)` +2. `ftruncate(fd, 4096)` +3. `mmap(NULL, 4096, RW, MAP_SHARED, fd, 0)` +4. Écrire 1024 u32 = pattern `0xDEADBEEF + index` +5. `munmap` + `close` +6. `shm_open("/redox-wl-test-03", O_RDWR)` — réouverture +7. `mmap` à nouveau +8. Vérifier le pattern bit à bit +9. `shm_unlink` + +### Résultat +```text +[test-03] phase1: shm_open created, fd=5 +[test-03] phase1: mmap at 0x15000 +[test-03] phase1: wrote 1024 u32 pixels +[test-03] phase2: shm_open reopened, fd=5 +[test-03] phase2: mmap at 0x15000 +[test-03] phase2: pattern verified across close/reopen +[test-03] cleanup: shm_unlink OK +[test-03] PASS: shm_open/mmap/persistence/unlink all work +``` + +**Notes** : +- Le pattern est intégralement préservé entre close/reopen → la zone + partagée vit indépendamment des fds qui la mappent (sémantique POSIX + correcte) +- `shm_unlink` retire effectivement la zone +- `mmap` retourne la même adresse (0x15000) sur les deux mappings — pas + une garantie POSIX mais cohérent + +**Conclusion** : Wayland peut utiliser `wl_shm_pool` via `shm_open`+`mmap` +sans adaptation. Pour `memfd_create` (préféré par libwayland-client moderne), +voir section "Manque restant" plus bas. + +--- + +## Test 4 — poll() multi-fd + +### Objectif +Valider le multiplexage `poll()` qui sera la base de l'event loop du +compositor (équivalent `wl_event_loop`). + +### Setup +Trois pipes (`ra/wa`, `rb/wb`, `rc/wc`). On `poll()` les trois read ends +avec POLLIN. Trois sous-tests : +- A : timeout 50ms sans data → doit retourner 0 +- B : `write(wb, "X")` → poll doit ne signaler que `rb` +- C : `close(wa)` → poll doit signaler ra (POLLHUP/POLLIN) + +### Résultat +```text +[test-04] A: timeout 50ms with no data → poll returned 0 OK +[test-04] B: poll detected only rb (index 1) ready, as expected +[test-04] B: read 1 byte 'X' from rb OK +[test-04] C: closing wa caused POLLHUP/POLLIN on ra (revents=0x1) +[test-04] PASS: poll() multiplexes correctly across fds and detects HUP +``` + +**Conclusion** : multiplexage fiable, comportement POSIX standard. +On peut soit utiliser `poll()` directement, soit s'appuyer sur +`redox_event::EventQueue` (utilisé par Orbital) qui wrappe le scheme +event Redox de manière plus idiomatique. + +--- + +## Manques restants à traiter + +### `memfd_create` non implémenté + +L'audit phase 1 avait noté : +> Numéro syscall défini (`__NR_memfd_create = 319`) mais pas vu d'impl +> dans relibc. + +Confirmé dans ce travail. **Impact** : libwayland-client moderne préfère +`memfd_create` aux `shm_open` (pas de path nécessaire, anti-leak en cas +de crash client). + +**Solutions possibles, par ordre de simplicité** : + +1. **Wrapper côté wayland-rs Redox** : ajouter un `cfg(target_os = "redox")` + qui force le path `shm_open` avec un nom unique (ex: `pid + counter`) +2. **Implémenter `memfd_create` dans relibc Redox** au-dessus de + `shm_open` + `shm_unlink` immédiat (pour rendre la zone anonyme) +3. **Ajouter un syscall Redox `memfd_create` natif** (le plus propre, + le plus long) + +Recommandation pour la phase 3 : option 1 dans un premier temps. Si on +upstream chez Redox plus tard, option 2. + +### Numéros de fd réutilisés en mono-process + +Constat du test 2 : pas vraiment un bug, mais à documenter dans +`compositor-architecture.md` pour qu'on n'écrive pas de tests qui +reposent sur des numéros distincts. + +### `pipe()` ne semble pas exposer pipes nommés FIFO + +Non testé ici. Pas critique pour Wayland (on utilise socket pairs). +À noter si on veut un jour porter du code Linux qui utilise des FIFO. + +--- + +## Conclusions pour la suite du plan + +### Phase 2 → ✅ TERMINÉE + +La phase 2 du plan visait : +> *Valider les mécanismes nécessaires au protocole Wayland.* + +Mission accomplie en **une session** au lieu des 6 mois budgétés. C'est +possible parce que l'audit phase 1 avait déjà localisé les implémentations +relibc — les tests n'avaient qu'à confirmer le comportement. + +### Implications pour le calendrier annuel + +Le plan directeur prévoyait année 1 = "audit + primitives". On a fait +les deux en deux sessions de 2h. **Année 1 peut donc démarrer la phase 3 +dès maintenant**, avec une marge confortable. + +### Premier ticket de phase 3 + +> Cloner `wayland-rs` upstream, ajouter target `x86_64-unknown-redox`, +> tenter `cargo build` sur les crates `wayland-scanner`, `wayland-backend`, +> `wayland-server`, `wayland-client`. Documenter les `cfg(unix)` qui ont +> besoin d'un `cfg(target_os = "redox")` distinct. + +--- + +## Code source des tests + +```text +~/Projets/Redox/redox-wayland-tests/ +├── test-01-unix-socket/ (40 bytes Wayland-shaped roundtrip) +├── test-02-fd-passing/ (mono-process, expose l'artefact) +├── test-02b-fd-passing-fork/ (multi-process via fork — la vraie validation) +├── test-03-shm-open/ (shm_open + mmap + persistance) +└── test-04-poll-multifd/ (poll multiplexing + POLLHUP) +``` + +À conserver en CI quand le repo `redox-wayland-compositor` sera créé +(phase 0 du plan, à venir). + +--- + +*Fin du document de phase 2.*