Compare commits

...
22 changed files with 2160 additions and 1052 deletions

No files matched your search

Generated
+819 -587
View File
File diff suppressed because it is too large. Load diff
+384
View File
@@ -0,0 +1,384 @@
{
"DEFINITIONS": {
"ANCHORED": "Förankrad",
"FLOATING": "Flytande",
"STATIC": "Stationär",
"SET": "Set",
"WATCH": "Klocka",
"DASHBOARD": "Kontrollpanel",
"EDIT_MODE": "Redigeringsläge",
"SETTINGS": "Inställningar",
"KEYBOARD": "Tangentbord",
"TASKBAR": "Uppgiftsfält",
"FEATURES": "Funktioner",
"OVERLAY": "Overlay",
"SKYMAP": "Skymap",
"PLAYSPACE": "Playspace",
"D_PAD": "D-pad"
},
"ACTIONS": {
"RECENTER_PLAYSPACE": "Centrera om playspace"
},
"APP_LAUNCHER": {
"ASPECT": {
"SEMI_TALL": "Halvhög",
"SEMI_WIDE": "Halvbredd",
"SQUARE": "Kvadratisk",
"TALL": "Hög",
"WIDE": "Bredbild"
},
"ASPECT_TITLE": "Bildförhållande",
"AUTOSTART": "Kör automatiskt vid start",
"LAUNCH": "Starta",
"MODE": {
"CAGE": "Kompatibilitetsläge (Cage)",
"NATIVE": "Nativt läge"
},
"RES_TITLE": "Upplösning"
},
"APP_SETTINGS": {
"ALLOW_SLIDING": "Stick-interaktion vid grepp",
"ANIMATION_SPEED": "Hastighet för UI-animationer",
"AUTOSTART_APPS": "Appar som ska köras vid start",
"BLOCK_GAME_INPUT": "Blockera spelindata",
"BLOCK_GAME_INPUT_HELP": "Blockerar all input när en overlay hovras över",
"BLOCK_GAME_INPUT_IGNORE_WATCH": "Ignorera klockan vid blockering av input",
"BLOCK_GAME_INPUT_IGNORE_WATCH_HELP": "Blockera inte inmatning när klockan är markerad",
"BLOCK_POSES_ON_KBD_INTERACTION": "Blockera poser vid interaktion med tangentbord",
"BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Blockerar spelets mottagning av poser när tangentbordet hovras över och 'Blockera spel-input' är aktiverat",
"BINDINGS": {
"COMP": {
"CLICK": "Klick",
"FORCE": "Tryckkraft",
"TOUCH": "Beröring",
"VALUE": "Värde",
"PROXIMITY": "Närhet",
"X_AXIS": "X-axel",
"Y_AXIS": "Y-axel"
},
"TYPE": {
"BUMPER": "Bumper",
"DPAD_UP": "D-pad upp",
"DPAD_DOWN": "D-pad ned",
"DPAD_LEFT": "D-pad vänster",
"DPAD_RIGHT": "D-pad höger",
"JOYSTICK": "Joystick",
"MENU": "Meny",
"SHOULDER": "Axel",
"SQUEEZE": "Grepp",
"SYSTEM": "System",
"THUMBREST": "Tumstöd",
"THUMBSTICK": "Thumbstick",
"TRACKPAD": "Trackpad",
"TRIGGER": "Trigger",
"VIEW": "Visa"
},
"CLICK": {
"ANY": "Vilken som helst",
"DOUBLE": "Dubbelklicka",
"TRIPLE": "Trippelklick",
"TYPE": "Antal tryck"
},
"ACTION": {
"CLICK": "Klicka*",
"GRAB": "Greppa*",
"ALT_CLICK": "Anpassat shell-kommando (ställ in alt_click i config)",
"SHOW_HIDE": "Visa, dölj, centrera om*",
"TOGGLE_DASHBOARD": "Växla instrumentpanel",
"SPACE_DRAG": "Dra playspace",
"SPACE_ROTATE": "Rotera playspace",
"SPACE_RESET": "Återställ Playspace",
"CLICK_MODIFIER_RIGHT": "Högerklick-modifierare",
"CLICK_MODIFIER_MIDDLE": "Modifierare för mittklick",
"MOVE_MOUSE": "Flytta mus (om avstängd som standard)",
"SCROLL": "Scrolla*"
},
"LEFT": "Vänster",
"RIGHT": "Höger",
"CHORD": "Kombination",
"CHORD_ADD_INPUT": "Lägg till inmatning för kombination",
"CHORD_INPUT_HAND": "Inmatningshand",
"COMPONENT": "Aktiveringstyp",
"SUBPATH": "Aktiverande kontroll",
"THRESHOLD": "Aktivera vid övre värdet\nAvaktivera vid nedre värdet"
},
"BROWSE_ONLINE_CATALOG": "Bläddra i onlinekatalogen...",
"BROWSE_SKYMAPS": "Bläddra bland skymaps",
"CAPTURE_METHOD": "Metod för skärmavbildning",
"CAPTURE_METHOD_HELP": "Prova att ändra detta om du\nupplever svarta eller glitchiga skärmar",
"COLOR_KEYING": "Färgnyckelning",
"COLOR_PALETTE": "Färgpalett",
"COLOR_PALETTE_HELP": "Klicka för att bläddra bland färgpaletter.\nSe webbplatsen för att skapa egna!",
"COLOR_PALETTES": "Färgpaletter",
"COLOR_PALETTE_CURRENT": "Nuvarande aktivt tema.",
"COLOR_PALETTE_CUSTOM_URL": "Lägg till ett eget anpassat tema!",
"COLOR_PALETTE_CUSTOM_URL2": "Klicka för att öppna webbmanualen.",
"COLOR_PALETTE_ACTIVATE": "Klicka för att använda detta tema.",
"CLEAR_PIPEWIRE_TOKENS": "Rensa PipeWire-tokens",
"CLEAR_PIPEWIRE_TOKENS_HELP": "Fråga om skärmval vid nästa start",
"CLEAR_SAVED_STATE": "Återställ sparade inställningar",
"CLEAR_SAVED_STATE_HELP": "Återställ sets och overlay-positioner",
"CLICK_FREEZE_TIME_MS": "Klickfrysningstid (ms)",
"CLICK_FREEZE_TIME_MS_HELP": "Hjälper till med precision vid dubbelklick",
"CLOCK_12H": "12-timmarsklocka",
"CONTROLS": "Kontroller",
"DELETE_ALL_CONFIGS": "Rensa konfiguration",
"DELETE_ALL_CONFIGS_HELP": "Ta bort alla konfigurationsfiler från conf.d",
"DOUBLE_CURSOR_FIX": "Fix för dubbla markörer",
"DOUBLE_CURSOR_FIX_HELP": "Aktivera om du ser två markörer",
"DEFAULT_OVERLAY_SCALE": "Standardstorlek för overlay",
"DEFAULT_OVERLAY_SCALE_HELP": "Skapa nya overlays i den här skalan.",
"DEFAULT_CURVATURE": "Standardkurvatur för overlay",
"DEFAULT_OPACITY": "Standardopacitet för overlay",
"DEFAULT_POSITIONING": "Standardposition för overlay",
"OVERLAY_DEFAULTS_HELP": "Gäller endast nya overlays",
"ENABLED": "Aktiverad",
"FEATURES": "Funktioner",
"FOCUS_FOLLOWS_MOUSE_MODE": "Musrörelse vid trigger-tryck",
"HANDSFREE_POINTER": "Handsfree-läge",
"HANDSFREE_POINTER_HELP": "Inmatning som används när rörelsekontroller saknas.\nVänster nyp är greppa, höger är klicka.",
"HANDSFREE_ALT_TAB": "Sikte för app-växlare",
"HANDSFREE_ALT_TAB_HELP": "Håll in Super+X, sikta på önskat fönster och släpp sedan för att byta fokus.",
"DIAGNOSTICS": {
"CPU_ACCESSIBLE_VRAM": "CPU-tillgängligt VRAM: {NUM} MiB",
"TITLE": "Diagnostik",
"ENABLE_REBAR": "Aktivera [resizable bar] i BIOS-inställningarna för optimal VR-prestanda"
},
"DIAGNOSTICS_REASON": {
"PACTL": "Krävs för kontroll av högtalare/mikrofon och byte av ljudkort",
"CAGE": "Rekommenderas vid inkompatibilitet med vissa X11-applikationer (kompatibilitetsläge)",
"XWAYLAND_SATELLITE": "Rekommenderas om du vill köra X11 (Xwayland)-applikationer i nativt läge",
"XDG_OPEN": "Krävs för att öppna URL:er eller Steam-spel"
},
"HIDE_GRAB_HELP": "Dölj grepphjälp",
"HIDE_USERNAME": "Dölj användarnamn",
"INPUT_EMULATION_METHOD": "Metod för inmatningssimulering",
"INPUT_EMULATION_METHOD_HELP": "Prova att ändra detta om mus- eller\ntangentbordindata inte registreras.",
"INPUT_CAPTURE_METHOD": "Metod för inmatningsfångst",
"INPUT_CAPTURE_METHOD_HELP": "Möjliggör användning av fysisk kringutrustning i WayVR.\n Super+Z: Växla input mellan WayVR och skrivbordet\n Super+X: Växla mellan WayVR-appar",
"INPUT_PROFILES": "Ändra styrinställningar",
"INPUT_PROFILES_HELP": "OpenXR-kontrollbindningar",
"INVERT_SCROLL_DIRECTION_X": "Invertera horisontell scrollriktning",
"INVERT_SCROLL_DIRECTION_Y": "Invertera vertikal rullningsriktning",
"LENS_SEPARATION_MM": "Linsavstånd (mm)",
"KEYBOARD_MIDDLE_CLICK": "Tangentbordets mittklick",
"KEYBOARD_MIDDLE_CLICK_HELP": "Modifier som används vid skrivning\nmed lila laser",
"KEYBOARD_SOUND_ENABLED": "Tangentbordsljud",
"LANGUAGE": "Språk",
"LEFT_HANDED_MOUSE": "Vänstermus",
"LEFT_HANDED_MOUSE_HELP": "Används om musknapparna är ombytta",
"LONG_PRESS_DURATION": "Långtryckstid",
"LOOK_AND_FEEL": "Utseende och känsla",
"MISC": "Övrigt",
"MIDDLE_GRABS_SINGLE": "Modifierare för mittklick greppar enskilda objekt",
"MIDDLE_GRABS_SINGLE_HELP": "Att greppa med modifieraren för mittklick (lila laser)\ngreppar enskilda overlays istället för hela setet",
"NOTIFICATIONS_ENABLED": "Aktivera aviseringar",
"NOTIFICATIONS_SOUND_ENABLED": "Ljud för aviseringar",
"NOT_SUPPORTED": "Ej stöds",
"NO_SKYMAPS_FOUND": "Inga skymaps hittades",
"OPAQUE_BACKGROUND": "Opak bakgrund",
"OPTION": {
"ANCHORED_HELP": "Följer centermarkören. Rekommenderas.",
"FLOATING_HELP": "Flyttas oberoende, centreras om.\nLätt att tappa bort; rekommenderas ej.",
"STATIC_HELP": "Håller sig på plats i förhållande till världen.",
"AUTO": "Automatisk",
"AUTO_HELP": "ScreenCopy GPU om det stöds,\nannars PipeWire GPU.",
"EYE_ONLY": "Ögonlaser",
"EYE_PINCH": "Öga + pinch",
"HMD_ONLY": "HMD-laser",
"HMD_PINCH": "HMD + pinch",
"NONE": "Ingen",
"NONE_INPUT_HELP": "Emulera inte mus och tangentbord.\nInteraktion med skärmar blir inte möjlig.",
"NONE_INPUT_CAPTURE_HELP": "Fånga inte mus och tangentbord.\nDu kommer inte att kunna använda fysisk mus\neller tangentbord i appar som startas i WayVR.",
"PIPEWIRE_HELP": "Snabb GPU-avbildning,\nstandard för alla skrivbord.",
"PW_FALLBACK_HELP": "Långsam metod med hög CPU-belastning.\nProva om PipeWire GPU inte fungerar",
"SCREENCOPY_GPU_HELP": "Snabb, inga popup-fönster för skärmdelning.\nFungerar på: Hyprland, Niri, River, Sway",
"SCREENCOPY_HELP": "Långsam, inga popup-fönster för skärmdelning.\nFungerar på: Hyprland, Niri, River, Sway",
"UINPUT": "Enhetsemulering",
"UINPUT_HELP": "Använder modulen uinput för att emulera en enhet.\nFungerar generellt bra på de flesta skrivbord.",
"EVDEV_HELP": "Ta kontroll över inmatningsenheter för exklusiv användning.\nAnvändaren måste tillhöra gruppen 'input'.",
"WL_VIRTUAL": "Wayland virtuell inmatning",
"WL_VIRTUAL_HELP": "Skickar händelser till Wayland-kompositorn via\nzwlr_virtual_pointer och zwp_virtual_keyboard.\nFungerar endast på vissa skrivbord.\nFungerar INTE på KDE, GNOME eller COSMIC."
},
"POINTER_ACCELERATION": "Musacceleration",
"POINTER_ACCELERATION_HELP": "På: Adaptiv musacceleration\nAv: Konstant hastighet",
"POINTER_LERP_FACTOR": "Utjämning av pekare",
"POINTER_SPEED": "Mushastighet",
"REQUIRES_RESTART": "Kräver omstart",
"RESET_PLAYSPACE": "Återställ playspace",
"RESET_PLAYSPACE_HELP": "Nollställ offset för playspace.",
"SAVE_PLAYSPACE_CENTER": "Spara playspace-center",
"SAVE_PLAYSPACE_CENTER_HELP": "Använd nuvarande playspace för bindningen \"återställ playspace\".",
"RESET_PLAYSPACE_CENTER": "Återställ playspace-center",
"RESET_PLAYSPACE_CENTER_HELP": "Rensa tidigare inställd mittpunkt för playspace.",
"RESTART_WAYVR": "Starta om WayVR",
"CLOSE_WAYVR": "Stäng WayVR",
"ROUND_MULTIPLIER": "UI-kanternas rundning",
"SCREEN_RENDER_DOWN": "Rendera skärm i lägre upplösning",
"SCREEN_RENDER_DOWN_HELP": "Hjälper mot aliasing på skärmar med hög upplösning",
"SCROLL_SPEED": "Scrollningshastighet",
"SELECT_VARIANT": "Välj variant",
"SNAP_ANGLE_DEG": "Snapvinkel för fönster (pitch & roll)",
"SNAP_ANGLE_DEG_HELP": "Greppade fönster snäpps till denna vinkel på pitch- och rollaxeln.",
"ENABLE_WATCH": "Aktivera klockan",
"SETS_ON_WATCH": "Set på klockan",
"SKYBOX": "Skymap",
"SKYMAP_ALREADY_DOWNLOADED": "Denna skymap är redan nedladdad. Välj åtgärd.",
"SPACE_DRAG": "Playspace",
"SPACE_DRAG_MULTIPLIER": "Multiplikator för space-drag",
"SPACE_DRAG_AFFECTS_WORLD": "Lämna overlays kvar vid flytt av playspace",
"SPACE_DRAG_UNLOCKED": "Tillåt space-drag på alla axlar",
"SPACE_GRAVITY_DAMPING": "Dämpning",
"SPACE_GRAVITY_DAMPING_HELP": "Artificiell dämpning för att sakta ner rörelser. 0,1 – hög dämpning, 1,0 – ingen dämpning",
"SPACE_GRAVITY_FLING_STRENGTH": "Kaststyrka",
"SPACE_GRAVITY_FLING_STRENGTH_HELP": "Intensitetsmultiplikator för gravitationskraften efter space-drag.\n0.0 - ingen rörelse alls, 2.0 - dubbel intensitet",
"SPACE_GRAVITY_GRAVITY": "Gravitation",
"SPACE_GRAVITY_GRAVITY_HELP": "Mängd nedåtgående kraft. 0.0 - ingen gravitation",
"SPACE_GRAVITY_GROUND_FRICTION": "Markfriktion",
"SPACE_GRAVITY_GROUND_FRICTION_HELP": "Mängden friktion som saktar ner dig när du rör vid marken.\n0.0 - ingen friktion (som på is), 1.0 - sträv yta",
"SPACE_GRAVITY_FLOOR_HEIGHT": "Golvhöjd",
"SPACE_GRAVITY_FLOOR_HEIGHT_HELP": "Y-positionen för golvet. Gravitationen upphör när denna höjd nås.",
"SPACE_ROTATE_UNLOCKED": "Tillåt rotation av playspace på alla axlar",
"ROTATIONS": "HMD-rotationer",
"SESSION_TIME": "Sessionstid",
"STATISTICS": "Statistik",
"TROUBLESHOOTING": "Felsökning",
"UI_GRADIENT_INTENSITY": "Intensitet för UI-gradient",
"UPRIGHT_SCREEN_FIX": "Fix för upprätt skärm",
"UPRIGHT_SCREEN_FIX_HELP": "Fixar upprätt ställda skärmar på vissa skrivbord",
"USE_PASSTHROUGH": "Aktivera passthrough",
"USE_PASSTHROUGH_HELP": "Tillåt passthrough om XR-runtime stöder det",
"USE_SKYBOX": "Visa skymap",
"USE_SKYBOX_HELP": "Visa en skymap om ingen scenapp eller passthrough används",
"WATCH_VIEW_ANGLE": "Klockans synvinklar",
"WATCH_VIEW_ANGLE_HELP": "Styr hur klockan tonas ut",
"WHISPER": {
"NEED_TO_DOWNLOAD_MODEL": "Den valda modellen måste laddas ner.\nVill du ladda ner den nu?",
"REMOVE_UNUSED_MODELS": "Du stänger av tal-till-text-funktionen.\nVill du ta bort nedladdade Whisper-modeller för att frigöra utrymme?"
},
"WHISPER_MODEL": "Tal-till-text-modell",
"WHISPER_MODEL_HELP": "Whisper-modell som ska användas.\nStörre modeller ger mer precisa\nresultat, men förbrukar mer VRAM.",
"SPEECH_TO_TEXT": "Tal till text",
"WHISPER_GPU_SELECT": "Välj GPU",
"TUTORIAL": "Handbok",
"GRID_OPACITY": "Golvgridens opacitet",
"GRID_OPACITY_HELP": "Opacitet för golvgridet när skymap är aktiverat",
"XWAYLAND_BY_DEFAULT": "Kör appar i kompatibilitetsläge som standard",
"SAVE": "Spara",
"CANCEL": "Avbryt"
},
"APPLICATION_LAUNCHER": "App-startare",
"APPLICATION_STARTED": "Applikationen startades",
"APPLICATIONS": "Applikationer",
"AUDIO": {
"SWITCH_TO_THIS_DEVICE": "Byt till den här enheten",
"AUTO_SWITCH_TO_VR_AUDIO": "Byt automatiskt till VR-ljud",
"CARDS": "Kort",
"FAILED_TO_SWITCH_MICROPHONE": "Kunde inte byta mikrofon",
"MICROPHONE_SET_SUCCESSFULLY": "Mikrofonen har ändrats",
"MICROPHONES": "Mikrofoner",
"NO_VR_MICROPHONE_SWITCH_MANUALLY": "Ingen VR-mikrofon hittades. Byt manuellt.",
"NO_VR_SPEAKERS_FOUND_SWITCH_MANUALLY": "Inga VR-högtalare hittades. Byt manuellt.",
"SELECT_AUDIO_CARD_PROFILE": "Välj ljudkortsprofil",
"SETTINGS": "Ljudinställningar",
"SPEAKERS": "Högtalare",
"SPEAKERS_SET_SUCCESSFULLY": "Högtalarna har ändrats",
"VOLUME": "Volym"
},
"CLOSE_WINDOW": "Stäng fönster",
"CREATION_DATE": "Skapad datum",
"DEBUG_INFO": "Debuginfo",
"DISPLAY_BRIGHTNESS": "Skärmljusstyrka",
"DONATE": {
"SUPPORT_US": "Stöd oss",
"DESCRIPTION": "Vi är ett litet team på två utvecklare som bygger WayVR på vår fritid. Om du gillar att använda programvaran, överväg att stötta oss – varje donation hjälper oss att fortsätta.",
"THANK_YOU_WAYVR": "Tack från WayVR-teamet!",
"BUTTON_TOOLTIP": "Donera via Open Collective",
"CURRENT_SUPPORTERS": "Nuvarande supportrar",
"BROUGHT_TO_YOU_VARIANTS_OFTEN": "Stöds av {USER}|Särskilt tack till {USER}|Sponsras av {USER}|Backas av {USER}|Drivs av community-medlemmar som {USER}|Möjliggjorts av {USER}|Upprätthålls av {USER}|Bidrag från {USER}|Finansierat av {USER}",
"BROUGHT_TO_YOU_VARIANTS_RARE": "Kaffe sponsrat av {USER}|Drivs av {USER}|Hälsningar till {USER}!|Håller oss igång: {USER}|Hjälper oss att växa: {USER}|Håller servrarna igång: {USER}|Med tacksamhet till {USER}|Drivs av {USER}|Stöds av vänner som {USER}",
"BROUGHT_TO_YOU_VARIANTS_VERY_RARE": "Stöttas av legender som {USER}|Koffein sponsrat av {USER}|En sann legend: {USER}|Dagens hjälte: {USER}|En total superstjärna: {USER}|Betalar elräkningen: {USER}|Räddaren av vårt förstånd: {USER}|Visar vägen: {USER}|Koffein-till-kod-omvandlare tillhandahållen av {USER}|Håller produktionen stabil: {USER}|Den ende och ende: {USER}|Skickar dmabuf-buffertar: {USER}"
},
"DOWNLOAD": "Ladda ner",
"DOWNLOADER": "Nedladdare",
"WELCOME": {
"ALL_SET": "Nu är du redo!",
"BASICS": "Dessa grunder gör att du enkelt kan börja använda WayVR.",
"CENTER_MARKER": "Centreringsmarkören visas framför dig vid omcentrering. Placera overlays runt den!",
"DOUBLE_PRESS_TO_TOGGLE": "Dubbeltryck B (på Index) eller Y (på Meta) för att visa, centrera om eller dölja ditt set. Prova nu.",
"FEATURES_DOCUMENTATION": "Det finns många andra funktioner värda att anpassa. För mer information, se dokumentationen på",
"FINISH_TUTORIAL": "Avsluta handledningen",
"GRAB_BOTH_HANDS": "Greppa med båda händerna om du vill flytta ett specifikt overlay i ditt set.",
"GRAB_ONE_HAND": "Greppa med en hand för att flytta ditt set.",
"GREETING": "Hej!",
"GUIDE_INTRO": "Låt oss guida dig genom dina första steg.",
"LASER_COLORS": "Laserfärger",
"LASER_FUNCTIONALITY": "Mycket av funktionaliteten i WayVR beror på vilken laserfärg som används för att interagera med ett UI-element. Du behöver inte trycka på knapparna, det räcker att röra vid dem!",
"LAST_PAGE_INFO": "Denna handledning slutar visas när du når den sista sidan.",
"LOOK_AT_LEFT_WRIST": "Titta på din vänstra handled. Det är din klocka.",
"MIDDLE_CLICK_MODE_PURPLE_LASER": "Mittenklick-läge: Lila laser",
"MOVING_YOUR_SET": "Flytta ditt set",
"NEXT_BUTTON": "Tryck på nästa-knappen nedan för att komma igång.",
"REGULAR_MODE_BLUE_LASER": "Vanligt läge: Blå laser",
"REPOSITION_THIS_WINDOW_BY_GRABBING": "Flytta det här fönstret med kontrollens grip-funktion.",
"RIGHT_CLICK_MODE_ORANGE_LASER": "Högerklicksläge: Orange laser",
"THANKS_FOR_INSTALLING": "Tack för att du installerade WayVR!",
"WATCH": "Klocka",
"WORKING_SET": "Aktivt Set"
},
"DOWNLOAD_AGAIN": "Ladda ner igen",
"DOWNLOADING_FILE": "Laddar ner fil...",
"FAILED_TO_LAUNCH_APPLICATION": "Kunde inte starta applikationen:",
"GAME_LAUNCHED": "Spelet har startats",
"GAME_LIST": {
"NO_GAMES_FOUND": "Inga spel hittades",
"RUNNING_GAMES_LIST": "Lista över körande spel"
},
"GAMES": "Spel",
"GENERAL_SETTINGS": "Allmänna inställningar",
"GETTING_STARTED": "Kom igång",
"HEIGHT": "Höjd",
"HELLO": "Hej!",
"HELLO_USER": "Hej, {USER}!",
"HIDE": "Dölj",
"HOME_SCREEN": "Hem",
"MODIFICATION_DATE": "Ändringsdatum",
"MONADO_RUNTIME": "Monado runtime",
"LOADING": "Laddar...",
"POPUP_ADD_DISPLAY": {
"RESOLUTION": "Upplösning"
},
"POWER_OPTIONS": {
"TITLE": "Strömalternativ",
"SHUTDOWN": "Stäng av",
"SUSPEND": "Viloläge",
"NOT_SUPPORTED": "Strömalternativ kräver systemd eller elogind.",
"SHUTDOWN_ARE_YOU_SURE": "Är du säker på att du vill stänga av datorn?",
"SUSPEND_ARE_YOU_SURE": "Är du säker på att du vill försätta datorn i viloläge?"
},
"PROCESS": {
"FORCE_KILL": "Tvinga avslut",
"STOP": "Stoppa"
},
"PROCESS_LIST": "Processlista",
"REFRESH": "Uppdatera",
"REMOVE": "Ta bort",
"APPLY": "Verkställ",
"RELOAD_FROM_DISK": "Ladda om från disk",
"SETTINGS": "Inställningar",
"SHOW": "Visa",
"TARGET_PATH": "Mål sökväg",
"TERMINATE_PROCESS": "Avsluta process",
"VERSION": "Version",
"WIDTH": "Bredd",
"PINNED_APPS": "Fästa applikationer",
"SAVED_TO_FAVOURITES": "Sparad i fästa applikationer",
"ADD_TO_FAVOURITES": "Lägg till i fästa applikationer",
"ADD_TO_FAVOURITES_HINT": "Spara förinställning och fäst den i app-menyn",
"REMOVE_FROM_FAVOURITES": "Ta bort från fästa applikationer",
"EMPTY": "Tomt",
"INSTALLED": "Installerad",
"NOT_INSTALLED": "Ej installerad",
"YES": "Ja",
"NO": "Nej"
}
+1
View File
@@ -31,4 +31,5 @@ TEMPLATE="de" bun main.ts
TEMPLATE="ja" bun main.ts
TEMPLATE="es" bun main.ts
TEMPLATE="it" bun main.ts
TEMPLATE="sv" bun main.ts
TEMPLATE="zh_CN" bun main.ts
+20
View File
@@ -0,0 +1,20 @@
{
"full_name": "Swedish",
"examples": [
{
"key": "BAR.OPACITY",
"en": "Opacity",
"translated": "Opacitet"
},
{
"key": "PANEL.CLOSE",
"en": "Close panel",
"translated": "Stäng panelen"
},
{
"key": "SETTINGS.LEFT_HANDED_MODE",
"en": "Left-handed mode",
"translated": "Vänsterhänt läge"
}
]
}
+185
View File
@@ -0,0 +1,185 @@
{
"DEFINITIONS": {
"ANCHORED": "Förankrad",
"FLOATING": "Flytande",
"STATIC": "Statisk",
"SET": "Set",
"WATCH": "Klocka",
"DASHBOARD": "Kontrollpanel",
"EDIT_MODE": "Redigeringsläge",
"MIRROR": "Spegling",
"SETTINGS": "Inställningar",
"KEYBOARD": "Tangentbord",
"TASKBAR": "Uppgiftspanel",
"FEATURES": "Funktioner",
"OVERLAY": "Overlay"
},
"ANCHOR": {
"CENTER": "Center"
},
"BAR": {
"ADD_MIRROR": "Lägg till ny spegling-overlay",
"ADD_NEW_SET": "Lägg till ny set",
"ADD_PASSTHRU": "Lägg till ny passthrough-utskärning",
"CLOSE_APP": "Stäng appen",
"CLOSE_OVERLAY": "Stäng {$DEFINITIONS.OVERLAY}",
"DELETE_CURRENT_SET": "Ta bort nuvarande set",
"EDIT_MODE_TOGGLE": "Växla redigeringsläge",
"FORCE_CLOSE_APP": "Tvinga stäng app",
"HANDSFREE": {
"EYE_ONLY": "Ögonsiktet",
"EYE_TRACKING": "Öga + pinch",
"HMD": "HMD + pinch",
"HMD_ONLY": "HMD-laser",
"NONE": "Av",
"TITLE": "Händsfree-läge"
},
"RELOAD_FROM_DISK": "Ladda om XML från disk",
"RESET_POSITION": "Återställ position",
"TOGGLE_VISIBILITY": "Växla synlighet"
},
"DEFAULT": "Standard",
"DISABLED": "Inaktiverat",
"DONE": "Klar",
"EDIT_MODE": {
"ADJUST_CURVATURE": "Justera krökning",
"ALIGN_TO_HMD": "Rikta mot HMD",
"ALPHA_BLEND_MODE": "Alfablendingsläge",
"ANGLE_FADE": "Vinkeltoning",
"ANGLE_FADE_HELP": "Tona ut när den inte är vänd mot HMD",
"BLENDING_ADDITIVE": "Additiv blandning",
"BLOCK_INPUT": "Blockera spel-input",
"CURVATURE": "Krökning",
"DELETE": "Håll inne för att ta bort från nuvarande set",
"DISABLE_GRAB": "Inaktivera grepp",
"GLOBAL": "Alltid synlig",
"HINT_POINT_WINDOW": "Peka på ett fönster för att ändra dess parametrar.\nNär du är klar, lämna redigeringsläget via knappen till höger.",
"INTERPOLATION": "Interpolation",
"KEYBOARD": "Tangentbord",
"LEAVE": "Lämna redigeringsläge",
"LOCK_INTERACTION": "Lås interaktion",
"MOUSE": {
"FLIP180": "Speglad 180°",
"FLIP270": "Speglad 270°",
"FLIP90": "Speglad 90°",
"FLIPPED": "Speglad",
"NORMAL": "Normal",
"ROTATE180": "Roterad 180°",
"ROTATE270": "Roterad 270°",
"ROTATE90": "Roterad 90°",
"TITLE": "Musfixar",
"WRONG_SCREEN_SELECTION_HELP": "Om markören rör sig på en helt annan skärm\nhar skärmarna troligtvis valts fel. Se readme."
},
"MOVE_PRESS_AND_DRAG": "Flytta (tryck & dra)",
"OPACITY": "Opacitet",
"POS_ANCHORED": "Förankrad: Flyttas tillsammans med set. Standard.",
"POS_FLOATING": "Flytande: Flyttas oberoende, centreras om vid visning.",
"POS_HAND_L": "Följ vänster hand.",
"POS_HAND_R": "Följ höger hand.",
"POS_HMD": "Följ HMD.",
"POS_STATIC": "Statisk: Inte en del av något set, ingen återcentrering.",
"POSITIONING": "Positionering",
"RESIZE_PRESS_AND_DRAG": "Ändra storlek (tryck & dra)",
"RESIZE_CORNER": "Dra för att ändra storlek",
"STEREO_3D_MODE": {
"ADJUST_MOUSE": "Justera mus",
"FULL_FRAME": "Full 3D",
"FULL_FRAME_BAT": "Full-BAT",
"FULL_FRAME_SBS": "Full-SBS",
"FULL_FRAME_TAB": "Full-TAB",
"SPLIT_BOTTOM_TOP": "NEDRE→ÖVRE",
"SPLIT_LEFT_RIGHT": "VÄNSTRER→HÖGER",
"SPLIT_RIGHT_LEFT": "HÖGER→VÄNSTER",
"SPLIT_TOP_BOTTOM": "TOPP→BOTTEN",
"TITLE": "3D-stereoläge"
}
},
"ALTTAB": {
"RELEASE_TO_FOCUS": "Sikta på en app och släpp Super för att fokusera på den.",
"WHILE_AIMING": "Vid sikte",
"CLICK": "VR-pekarklick",
"GRABALL": "Flytta hela setet",
"GRAB": "Flytta enskild overlay",
"DASH": "Växla WayVR Dashboard",
"EDIT": "Växla redigeringsläge",
"KEYBOARD": "Växla tangentbord",
"SET": "Byt set",
"RECENTER": "Visa/dölj/centrera",
"GENERAL_BINDS": "Allmänna bindningar",
"TOGGLE_CAPTURE": "Återlämna kontrollen till skrivbordet",
"FOCUS_MODE": "Fokusläge (håll in Super)",
"CLOSE": "Stäng fokuserad app",
"MOUSE": "Klick och skroll med fysisk mus översätts även till VR-pekaren"
},
"GRAB": {
"ADJUST_DISTANCE": "Justera avstånd",
"ADJUST_SIZE": "Justera storlek",
"GRABBING_ANCHORED": "Förankrade överlägg rör sig med setet. Separera ett enskilt fönster genom att greppa det med den andra handen samtidigt som du håller i setet.",
"GRABBING_ANCHORED_EDIT": "Det här överlägget kommer att förbli förankrat vid centreringsmarkören.",
"GRABBING_FLOATING": "Det här överlägget är flytande och kommer att stanna på plats, om det inte centreras om.",
"GRABBING_FOLLOW": "Detta overlay följer den enhet den är kopplad till.",
"GRABBING_STATIC": "Det här överlägget är statiskt och kommer att stanna kvar på platsen, oavsett om du centrerar om.",
"GRABBING_WATCH": "För att byta hand, flytta klockan framför dig och greppa den med den andra handen.",
"UNRESTRICTED_MOVEMENT": "Obegränsad rörelse"
},
"SCREENCAST": {
"PENDING": "Vänligen vänta...",
"QUEUED": "Väntar på att en annan overlays skärmdelningsförfrågan ska godkännas.",
"WAITING_FOR_USER_DESC": "Kontrollera skrivbordet efter ett popup-fönster för skärmdelning.\nVälj följande:",
"WAITING_FOR_USER": "Kontrollera din skrivbordsdisplay för ett popup-fönster om skärmdelning.",
"FAIL": "Kunde inte initiera källan för skärmöverföring."
},
"TOAST": {
"TITLE_SCREENCAST_FAIL": "Kunde inte starta källan för skärmdelning!",
"CANNOT_ADD_SET": "Kunde inte lägga till set!",
"CANNOT_REMOVE_SET": "Kan inte ta bort set!",
"STEAMVR_DASH_TITLE": "SteamVR-kontrollpanelen är öppen",
"STEAMVR_DASH_TEXT": "Stäng SteamVR-panelen för att börja använda WayVR.",
"WAYVR_RUNNING_TITLE": "WayVR körs!",
"WAYVR_RUNNING_TEXT": "Kolla handleden för klockan, eller använd bindningen för visa/dölj för att komma igång!",
"DEFAULT_TITLE": "Avisering",
"EMPTY_SET": "Tomt set!",
"ERROR": "Fel",
"FIXING_FLOOR_IN_X_SECS": "Justerar golvet om {SECONDS} sekunder...",
"LAST_EXISTING_SET": "Detta är det sista befintliga setet.",
"LETS_ADD_OVERLAYS": "Låt oss lägga till några overlays från uppgiftspanelen!",
"MAXIMUM_SETS_REACHED": "Maximalt antal set har uppnåtts.",
"NO_KEYMAPS_CONFIGURED": "Inga tangentbordslayouts konfigurerade",
"NO_KEYMAPS_CONFIGURED_HELP": "Kolla wayvr.org för att se hur du ställer in dina tangentbordslayouter!",
"NO_SET_SELECTED": "Inget Set är valt.",
"ONE_CONTROLLER_ON_FLOOR": "Placera en kontroller på golvet!"
},
"WATCH": {
"ADD_NEW_SET": "Lägg till ett nytt set",
"CLEANUP_MIRRORS": "Ta bort speglingar som\ninte är synliga just nu",
"DASHBOARD": "Kontrollpanel",
"EDIT_MODE": "Redigeringsläge",
"FIX_FLOOR": "Justera golvnivå",
"LONG_PRESS_TO_DELETE_SET": "Håll för att ta bort",
"RECENTER": "Centrera playspace",
"SWITCH_TO_SET": "Byt till set",
"TOGGLE_FOR_CURRENT_SET": "Växla för nuvarande set"
},
"WHISPER": {
"TRANSCRIBE": "Håll ned för att transkribera text",
"PASTE": "Kopiera transkribering till urklipp",
"PASTE_AND_GO": "Kopiera transkribering och skicka Ctrl-V till fokuserad app",
"SEND_OSC": "Skicka transkription till OSC-chatt",
"UNLOAD_AND_CLOSE": "Avlasta modell och stäng",
"MODEL_NOT_DOWNLOADED": "Tal-till-text är inte konfigurerat!",
"DOWNLOAD_GUIDANCE": "Konfigurera tal-till-text under inställningar, funktioner!",
"INITIAL_TEXT": "Håll inne mikroknappen för att börja transkribera...",
"INIT_ERROR": "Fel vid start av Whisper-motorn",
"ENTER_KEY": "Enter-tangent"
},
"NOTIFICATION": {
"COUD_NOT_CONNECT_TO_VR_RUNTIME": "Kunde inte ansluta till VR-runtime.",
"NOT_RUNNING_XR_VR": "Se till att Monado, WiVRn eller SteamVR är igång.",
"NOT_RUNNING_VR": "Se till att Monado, WiVRn eller SteamVR är igång.",
"NOT_RUNNING_XR": "Se till att Monado eller WiVRn är igång.",
"INVALID_ARGS": "Kontrollera dina startargument.",
"SELECT_SCREEN_CAST_FOR": "Välj skärmdelning för display {DISPLAY}",
"CANNOT_CAPTURE_KBD_MOUSE": "Kunde inte initiera fångst av tangentbord/mus!\nKontrollera att din användare tillhör gruppen 'input'.",
"WE_ARE_GRABBING": "WayVR styr nu ditt tangentbord och din mus!\nTryck Super+Z för att släppa."
}
}
+3 -1
View File
@@ -114,6 +114,8 @@ impl ImageImporter {
num_planes: dmabuf.num_planes(),
planes: Default::default(),
mouse: None,
acquire_sync: Default::default(),
lease: None,
};
for (i, handle) in dmabuf.handles().enumerate() {
@@ -129,7 +131,7 @@ impl ImageImporter {
frame.planes[i].stride = stride as _;
}
let image = self.gfx.dmabuf_texture(frame)?;
let image = self.gfx.dmabuf_texture(&frame)?;
log::info!("DMA-buf import: creating image view");
let image_view = self.gfx.create_image_view(image)?;
log::info!("DMA-buf import: image view created");
+2 -2
View File
@@ -4,11 +4,11 @@ use wgui::gfx::{DmabufImportInfo, DmabufPlaneLayout, Format, Image, ImageView, W
use wlx_capture::{DrmFormat, DrmFourcc, DrmModifier, frame::DmabufFrame};
pub trait WGfxDmabuf {
fn dmabuf_texture(&self, frame: DmabufFrame) -> anyhow::Result<Arc<Image>>;
fn dmabuf_texture(&self, frame: &DmabufFrame) -> anyhow::Result<Arc<Image>>;
}
impl WGfxDmabuf for WGfx {
fn dmabuf_texture(&self, frame: DmabufFrame) -> anyhow::Result<Arc<Image>> {
fn dmabuf_texture(&self, frame: &DmabufFrame) -> anyhow::Result<Arc<Image>> {
let format = fourcc_to_vk(frame.format.drm_format.code)?;
let modifier = (!matches!(frame.format.drm_format.modifier, DrmModifier::Invalid))
.then(|| u64::from(frame.format.drm_format.modifier));
+17
View File
@@ -8,6 +8,8 @@ use wgui::gfx::{
BufferUsage, BuiltCommandBuffer, Format, ShaderModule, Vert2Buf, Vert2Uv, WGfx, WGfxCreateInfo,
};
use wlx_capture::DrmFormat;
#[cfg(feature = "pipewire")]
use wlx_capture::syncobj::DrmRenderNode;
use crate::shaders::{
frag_color, frag_grid, frag_screen, frag_simple, frag_sky, frag_srgb, frag_wayland, vert_quad,
@@ -20,6 +22,8 @@ pub struct WGfxExtras {
pub drm_formats: Arc<[DrmFormat]>,
pub quad_verts: Vert2Buf,
pub drm_device: Option<(i64, i64)>,
#[cfg(feature = "pipewire")]
pub drm_render_node: Option<DrmRenderNode>,
}
impl WGfxExtras {
@@ -55,19 +59,32 @@ impl WGfxExtras {
];
let quad_verts = gfx.new_buffer(BufferUsage::VERTEX_BUFFER, &vertices)?;
#[cfg(feature = "pipewire")]
let drm_device = gfx.device_info().drm_render_node.map(|(major, minor)| {
log::info!("DRM render device: {major} {minor}");
(i64::from(major), i64::from(minor))
});
#[cfg(feature = "pipewire")]
if drm_device.is_none() {
log::warn!("No DRM device.");
}
#[cfg(feature = "pipewire")]
let drm_render_node = drm_device.and_then(|(maj, min)| {
use wgui::log::LogErr;
use wlx_capture::syncobj::open_render_node;
open_render_node(maj as u32, min as u32)
.log_err("Could not open DRM render node")
.ok()
});
Ok(Self {
shaders,
drm_formats,
quad_verts,
drm_device,
#[cfg(feature = "pipewire")]
drm_render_node,
})
}
}
+24 -2
View File
@@ -48,10 +48,12 @@ pub(super) enum CaptureType {
pub struct ScreenBackend {
name: Arc<str>,
// frames mut come before `capture` to maintain drop order
cur_frame: Option<WlxCaptureOut>,
pending_frame: Option<WlxCaptureOut>,
capture_type: CaptureType,
capture: Box<dyn WlxCapture<WlxCaptureIn, WlxCaptureOut>>,
pipeline: Option<ScreenPipeline>,
cur_frame: Option<WlxCaptureOut>,
meta: Option<FrameMeta>,
mouse_transform: Affine2,
interaction_transform: Option<Affine2>,
@@ -77,6 +79,7 @@ impl ScreenBackend {
capture,
pipeline: None,
cur_frame: None,
pending_frame: None,
meta: None,
mouse_transform: Affine2::ZERO,
interaction_transform: None,
@@ -204,7 +207,26 @@ impl OverlayBackend for ScreenBackend {
return Ok(ShouldRender::Unable);
}
if let Some(frame) = self.capture.receive() {
// don't submit the new image until the producer signaled
if let Some(mut frame) = self.capture.receive()
&& (self.pending_frame.is_none() || matches!(frame.is_ready(), Ok(true)))
{
self.pending_frame = Some(frame);
}
let pending_ready = match self.pending_frame.as_mut() {
Some(frame) => match frame.is_ready() {
Ok(ready) => ready,
Err(e) => {
log::warn!("{}: failed to check DMA-buf acquire fence: {e}", self.name);
self.pending_frame = None;
false
}
},
None => false,
};
if pending_ready {
let frame = self.pending_frame.take().unwrap();
let stereo = self.stereo.unwrap_or(StereoMode::None);
let mut meta = frame.get_frame_meta(&app.session.config, stereo);
+19 -2
View File
@@ -558,9 +558,15 @@ pub(super) struct WlxCaptureOut {
pub(super) image: Arc<ImageView>,
pub(super) format: FrameFormat,
pub(super) mouse: Option<MouseMeta>,
pub(super) acquire_sync: wlx_frame::FrameAcquireSync,
pub(super) _lease: Option<wlx_frame::FrameLease>,
}
impl WlxCaptureOut {
pub(super) fn is_ready(&mut self) -> std::io::Result<bool> {
self.acquire_sync.check_ready()
}
pub(super) fn get_frame_meta(&self, config: &GeneralConfig, stereo: StereoMode) -> FrameMeta {
FrameMeta {
clear: WGfxClearMode::DontCare,
@@ -719,18 +725,23 @@ fn upload_image_with_row_length(
pub(super) fn receive_callback(me: &WlxCaptureIn, frame: WlxFrame) -> Option<WlxCaptureOut> {
match frame {
WlxFrame::Dmabuf(frame) => {
WlxFrame::Dmabuf(mut frame) => {
if !frame.is_valid() {
log::error!("{}: Invalid frame", me.name);
return None;
}
log::trace!("{}: New DMA-buf frame", me.name);
let format = frame.format;
match me.gfx.dmabuf_texture(frame) {
let lease = frame.lease.clone();
let acquire_sync = std::mem::take(&mut frame.acquire_sync);
match me.gfx.dmabuf_texture(&frame) {
Ok(image) => Some(WlxCaptureOut {
image: me.gfx.create_image_view(image).ok()?,
format,
mouse: None,
acquire_sync,
_lease: lease,
}),
Err(e) => {
log::error!("{}: Failed to create DMA-buf vkImage: {}", me.name, e);
@@ -766,6 +777,8 @@ pub(super) fn receive_callback(me: &WlxCaptureIn, frame: WlxFrame) -> Option<Wlx
image: me.gfx.create_image_view(image).ok()?,
format: frame.format,
mouse: None,
acquire_sync: wlx_frame::FrameAcquireSync::Ready,
_lease: None,
})
}
WlxFrame::MemPtr(frame) => {
@@ -800,6 +813,8 @@ pub(super) fn receive_callback(me: &WlxCaptureIn, frame: WlxFrame) -> Option<Wlx
image: me.gfx.create_image_view(image).ok()?,
format: frame.format,
mouse: frame.mouse,
acquire_sync: wlx_frame::FrameAcquireSync::Ready,
_lease: None,
})
}
WlxFrame::Implicit(transform) => {
@@ -823,6 +838,8 @@ pub(super) fn receive_callback(me: &WlxCaptureIn, frame: WlxFrame) -> Option<Wlx
image,
format,
mouse: None,
acquire_sync: wlx_frame::FrameAcquireSync::Ready,
_lease: None,
})
}
}
+8 -2
View File
@@ -344,8 +344,14 @@ fn check(mut par: CheckParams, finalize_fn: ScreenCastFinalizeFn) {
par.app.tasks.enqueue(TaskType::Overlay(OverlayTask::Modify(
OverlaySelector::Name(par.name.clone()),
Box::new(move |app, owc| {
let capture =
new_wlx_capture!(app, PipewireCapture::new(par.name.clone(), node_id));
let capture = new_wlx_capture!(
app,
PipewireCapture::new(
par.name.clone(),
node_id,
app.gfx_extras.drm_render_node.clone()
)
);
owc.backend =
finalize_fn(par.name, par.logical_pos, par.logical_size, capture, app);
+2 -1
View File
@@ -16,6 +16,7 @@ idmap = { workspace = true }
libc.workspace = true
log = { workspace = true }
pipewire = { workspace = true, optional = true }
timeline_syncobj = { git = "https://github.com/Schmarni-Dev/timeline_syncobj", rev = "7f67e49ebf466487cf35138fc108618092acb9e8", optional = true }
slotmap.workspace = true
wayland-client = { workspace = true, optional = true }
wayland-protocols = {
@@ -46,7 +47,7 @@ smithay-client-toolkit = { version = "0.20.0", optional = true }
[features]
default = ["pipewire", "wayland"]
egl = []
pipewire = ["dep:dbus", "dep:pipewire", "wayland"]
pipewire = ["dep:dbus", "dep:pipewire", "dep:timeline_syncobj", "wayland"]
wayland = [
"dep:smithay-client-toolkit",
"dep:wayland-client",
-11
View File
@@ -30,17 +30,6 @@ let capture = PipewireCapture::new(
);
```
### Wlr-Dmabuf Setup
```rust
let wl = WlxClient::new();
// select desired screen
let output_id = wl.outputs.keys().first().unwrap();
let mut capture = WlrDmabufCapture::new(wl, *output_id).unwrap();
```
### XSHM Setup
```rust
let monitors = XshmCapture::get_monitors();
+110 -1
View File
@@ -1,4 +1,10 @@
use std::os::fd::RawFd;
use std::{
os::fd::{AsRawFd, OwnedFd, RawFd},
sync::Arc,
};
#[cfg(feature = "pipewire")]
use timeline_syncobj::timeline_syncobj::TimelineSyncObj;
use drm_fourcc::{DrmFormat, DrmFourcc, DrmModifier};
@@ -69,6 +75,8 @@ pub struct DmabufFrame {
pub num_planes: usize,
pub planes: [FramePlane; 4],
pub mouse: Option<MouseMeta>,
pub acquire_sync: FrameAcquireSync,
pub lease: Option<FrameLease>,
}
impl DmabufFrame {
@@ -139,6 +147,107 @@ pub struct MouseMeta {
pub y: f32,
}
#[derive(Clone)]
pub struct FrameLease(Arc<FrameLeaseInner>);
struct FrameLeaseInner(Box<dyn Fn() + Send + Sync>);
impl FrameLease {
pub fn new(f: impl Fn() + Send + Sync + 'static) -> Self {
Self(Arc::new(FrameLeaseInner(Box::new(f))))
}
}
impl Drop for FrameLeaseInner {
fn drop(&mut self) {
(self.0)();
}
}
#[derive(Clone, Default)]
pub enum FrameAcquireSync {
#[default]
/// Frame is ready, no sync needed
Ready,
/// Implicit sync using polled FDs
Implicit { fds: Arc<[OwnedFd]> },
#[cfg(feature = "pipewire")]
/// Explicit sync using DRM SyncObj
Explicit {
timeline: Arc<TimelineSyncObj>,
point: u64,
},
}
impl FrameAcquireSync {
pub fn check_ready(&mut self) -> std::io::Result<bool> {
match self {
FrameAcquireSync::Ready => Ok(true),
FrameAcquireSync::Implicit { fds } => {
if fds.is_empty() {
return Err(std::io::Error::other("implicit DMA-buf without planes"));
}
for plane_fd in fds.iter() {
let fd = plane_fd.as_raw_fd();
let mut pfd = libc::pollfd {
fd,
events: libc::POLLIN,
revents: 0,
};
let result = unsafe { libc::poll(&mut pfd, 1, 0) };
if result < 0 {
let error = std::io::Error::last_os_error();
if error.kind() == std::io::ErrorKind::Interrupted {
return Ok(false);
}
return Err(error);
}
if result == 0 {
return Ok(false);
}
if pfd.revents & (libc::POLLNVAL | libc::POLLHUP) != 0 {
return Err(std::io::Error::other(format!(
"DMA-buf plane fd {fd} poll returned with {:#x}",
pfd.revents
)));
}
if pfd.revents & libc::POLLERR != 0 {
static WARNED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
if !WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) {
log::error!(
"Could not poll implicit acquire fence. Falling back to unsynchronized behavior. Visual artifacts may happen."
);
}
continue;
}
if pfd.revents & libc::POLLIN == 0 {
return Ok(false);
}
}
*self = FrameAcquireSync::Ready;
Ok(true)
}
#[cfg(feature = "pipewire")]
FrameAcquireSync::Explicit { timeline, point } => {
if *point == 0 {
return Ok(true);
}
if timeline
.query_signaled_point()
.map(|signaled| signaled >= *point)
.map_err(|e| std::io::Error::from_raw_os_error(e.raw_os_error()))?
{
*self = FrameAcquireSync::Ready;
Ok(true)
} else {
Ok(false)
}
}
}
}
}
pub trait DmaExporter {
fn next_frame(
&mut self,
+3 -3
View File
@@ -6,12 +6,12 @@ use frame::WlxFrame;
pub mod frame;
#[cfg(feature = "pipewire")]
pub mod syncobj;
#[cfg(feature = "wayland")]
pub mod wayland;
#[cfg(feature = "wlr")]
pub mod wlr_dmabuf;
#[cfg(feature = "wlr")]
pub mod wlr_screencopy;
+346 -163
View File
@@ -1,5 +1,9 @@
use std::any::Any;
use std::cell::RefCell;
use std::collections::HashMap;
use std::os::fd::{FromRawFd, OwnedFd, RawFd};
use std::ptr::NonNull;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::mpsc;
use std::thread::JoinHandle;
@@ -10,8 +14,9 @@ use drm_fourcc::DrmModifier;
use pipewire as pw;
use pipewire::context::ContextRc;
use pipewire::main_loop::MainLoopRc;
use pipewire::spa::buffer::meta::MetaVideoTransformValue;
use pipewire::spa::buffer::meta::{MetaCursor, MetaHeader, MetaHeaderFlags, MetaVideoTransform};
use pipewire::spa::buffer::meta::{
MetaCursor, MetaHeader, MetaHeaderFlags, MetaVideoTransform, MetaVideoTransformValue,
};
use pipewire::stream::StreamRc;
use pw::spa;
@@ -21,7 +26,7 @@ use pw::stream::{Stream, StreamFlags};
use spa::buffer::DataType;
use spa::param::ParamType;
use spa::param::video::VideoFormat;
use spa::param::video::VideoInfoRaw;
use spa::param::video::{VideoFlags, VideoInfoRaw};
use spa::pod::ChoiceValue;
use spa::pod::Pod;
use spa::pod::serialize::GenError;
@@ -29,13 +34,24 @@ use spa::pod::{Object, Property, PropertyFlags, Value};
use spa::utils::Choice;
use spa::utils::ChoiceEnum;
use spa::utils::ChoiceFlags;
use timeline_syncobj::render_node::DrmRenderNode;
use wayland_client::backend::smallvec::SmallVec;
use super::raw_buffer::DequeuedBuffer;
use crate::WlxCapture;
use crate::frame::FrameFormat;
use crate::frame::MouseMeta;
use crate::frame::Transform;
use crate::frame::WlxFrame;
use crate::frame::{DmabufFrame, FramePlane, MemFdFrame, MemPtrFrame};
use crate::frame::{
DmabufFrame, FrameAcquireSync, FrameLease, FramePlane, MemFdFrame, MemPtrFrame,
};
use crate::pipewire::raw_buffer::{HeldBuffers, MAX_PW_BUFFERS};
use crate::syncobj::import_timeline;
const SPA_DATA_SYNCOBJ: u32 = 5;
const SPA_META_SYNC_TIMELINE: u32 = 9;
const SPA_PARAM_BUFFERS_META_TYPE: u32 = 7;
#[derive(Default)]
struct StreamData {
@@ -48,6 +64,7 @@ pub enum PwChangeRequest {
Pause,
Resume,
Stop,
ReleaseBuffer(usize),
}
struct CaptureData<R>
@@ -66,18 +83,20 @@ where
data: Option<CaptureData<R>>,
node_id: u32,
handle: Option<JoinHandle<Result<(), Error>>>,
drm_render_node: Option<DrmRenderNode>,
}
impl<R> PipewireCapture<R>
where
R: Any + Send,
{
pub fn new(name: Arc<str>, node_id: u32) -> Self {
pub fn new(name: Arc<str>, node_id: u32, drm_render_node: Option<DrmRenderNode>) -> Self {
PipewireCapture {
name,
data: None,
node_id,
handle: None,
drm_render_node,
}
}
}
@@ -87,7 +106,8 @@ where
R: Any + Send,
{
fn drop(&mut self) {
if let Some(data) = &self.data {
if let Some(data) = self.data.take() {
drop(data.rx_frame);
let _ = data.tx_ctrl.send(PwChangeRequest::Stop);
}
if let Some(handle) = self.handle.take() {
@@ -107,10 +127,15 @@ where
user_data: U,
receive_callback: fn(&U, WlxFrame) -> Option<R>,
) {
let (tx_frame, rx_frame) = mpsc::sync_channel(2);
let (tx_frame, rx_frame) = mpsc::sync_channel(4);
let (tx_ctrl, rx_ctrl) = pw::channel::channel();
self.data = Some(CaptureData { tx_ctrl, rx_frame });
self.data = Some(CaptureData {
tx_ctrl: tx_ctrl.clone(),
rx_frame,
});
let drm_render_node = self.drm_render_node.clone();
self.handle = Some(std::thread::spawn({
let name = self.name.clone();
@@ -123,9 +148,11 @@ where
node_id,
formats,
tx_frame,
tx_ctrl,
rx_ctrl,
user_data,
receive_callback,
drm_render_node,
)
}
}));
@@ -175,10 +202,12 @@ fn main_loop<U, R>(
name: Arc<str>,
node_id: u32,
dmabuf_formats: Vec<DrmFormat>,
sender: mpsc::SyncSender<R>,
receiver: pw::channel::Receiver<PwChangeRequest>,
tx_frame: mpsc::SyncSender<R>,
tx_ctrl: pw::channel::Sender<PwChangeRequest>,
rx_ctrl: pw::channel::Receiver<PwChangeRequest>,
user_data: U,
receive_callback: fn(&U, WlxFrame) -> Option<R>,
drm_render_node: Option<DrmRenderNode>,
) -> Result<(), Error>
where
U: Any,
@@ -189,6 +218,8 @@ where
let context = ContextRc::new(&main_loop, None)?;
let core = context.connect_rc(None)?;
let held_buffers = Rc::new(RefCell::new(HeldBuffers::default()));
let stream = StreamRc::new(
core,
&name,
@@ -217,6 +248,7 @@ where
})
.param_changed({
let name = name.clone();
let support_explicit_sync = drm_render_node.is_some();
move |stream, format, id, param| {
let Some(param) = param else {
return;
@@ -234,186 +266,278 @@ where
format.drm_format.code = spa_to_fourcc(info.format());
format.drm_format.modifier = DrmModifier::from(info.modifier());
let kind = if info.modifier() != 0 {
"DMA-buf"
} else {
"SHM"
};
let dmabuf = info.flags().contains(VideoFlags::MODIFIER);
let kind = if dmabuf { "DMA-buf" } else { "SHM" };
log::info!("{}: got {} video format:", name, kind);
log::info!(" format: {} ({:?})", info.format().as_raw(), info.format());
log::info!(" size: {}x{}", info.size().width, info.size().height);
log::info!(" modifier: {}", info.modifier());
let Ok(params_bytes) = obj_to_bytes(get_buffer_params()) else {
log::warn!("{}: failed to serialize buffer params", name);
return;
};
let Some(params_pod) = Pod::from_bytes(&params_bytes) else {
log::warn!("{}: failed to deserialize buffer params", name);
return;
};
let header_bytes = obj_to_bytes(get_meta_object(
spa::sys::SPA_META_Header,
std::mem::size_of::<spa::sys::spa_meta_header>(),
))
.unwrap(); // want panic
let header_pod = Pod::from_bytes(&header_bytes).unwrap(); // want panic
let mut param_bytes = Vec::new();
if dmabuf && support_explicit_sync {
param_bytes.push(obj_to_bytes(get_sync_buffer_params()).unwrap());
// order defines preference, we prefer explicit sync
param_bytes.push(
obj_to_bytes(get_meta_object(
SPA_META_SYNC_TIMELINE,
std::mem::size_of::<MetaSyncTimeline>(),
))
.unwrap(),
);
}
param_bytes.push(obj_to_bytes(get_buffer_params()).unwrap());
param_bytes.push(
obj_to_bytes(get_meta_object(
spa::sys::SPA_META_Header,
std::mem::size_of::<spa::sys::spa_meta_header>(),
))
.unwrap(),
);
param_bytes.push(
obj_to_bytes(get_meta_object(
spa::sys::SPA_META_VideoTransform,
std::mem::size_of::<spa::sys::spa_meta_videotransform>(),
))
.unwrap(),
);
let xform_bytes = obj_to_bytes(get_meta_object(
spa::sys::SPA_META_VideoTransform,
std::mem::size_of::<spa::sys::spa_meta_videotransform>(),
))
.unwrap(); // want panic
let xform_pod = Pod::from_bytes(&xform_bytes).unwrap(); // want panic
let mut pods = [params_pod, header_pod, xform_pod];
let mut pods: Vec<&Pod> = param_bytes
.iter()
.map(|bytes| Pod::from_bytes(bytes).unwrap())
.collect();
if let Err(e) = stream.update_params(&mut pods) {
log::error!("{}: failed to update params: {}", name, e);
}
}
})
.remove_buffer({
let held_buffers = held_buffers.clone();
let name = name.clone();
move |_, _, ptr| {
if let Some(buffer) = NonNull::new(ptr)
&& held_buffers.borrow_mut().on_removed(buffer)
{
log::warn!("{name}: PipeWire removed a still-leased buffer");
}
}
})
.process({
let name = name.clone();
let sync_device = drm_render_node.clone();
let held_buffers = held_buffers.clone();
let tx_ctrl = tx_ctrl.clone();
let u = user_data;
move |stream, format| {
let mut maybe_buffer = None;
// discard all but the newest frame
while let Some(buffer) = stream.dequeue_buffer() {
maybe_buffer = Some(buffer);
let mut new_buffer = None;
while let Some(buffer) = DequeuedBuffer::dequeue(stream) {
new_buffer = Some(buffer);
}
let Some(buffer) = new_buffer else { return };
if let Some(header) = buffer.find_meta::<MetaHeader>()
&& header.flags().contains(MetaHeaderFlags::CORRUPTED)
{
log::warn!("{name}: PipeWire buffer is corrupt");
return;
}
if let Some(mut buffer) = maybe_buffer {
if let Some(header) = buffer.find_meta::<MetaHeader>()
&& header.flags().contains(MetaHeaderFlags::CORRUPTED)
{
log::warn!("{}: PipeWire buffer is corrupt.", name);
return;
}
if let Some(transform) = buffer.find_meta::<MetaVideoTransform>() {
format.transform = match transform.transform() {
MetaVideoTransformValue::NONE => Transform::Normal,
MetaVideoTransformValue::ROTATED90 => Transform::Rotated90,
MetaVideoTransformValue::ROTATED180 => Transform::Rotated180,
MetaVideoTransformValue::ROTATED270 => Transform::Rotated270,
MetaVideoTransformValue::FLIPPED => Transform::Flipped,
MetaVideoTransformValue::FLIPPED90 => Transform::Flipped90,
MetaVideoTransformValue::FLIPPED180 => Transform::Flipped180,
MetaVideoTransformValue::FLIPPED270 => Transform::Flipped270,
_ => Transform::Undefined,
};
}
if let Some(transform) = buffer.find_meta::<MetaVideoTransform>() {
format.transform = match transform.transform() {
MetaVideoTransformValue::NONE => Transform::Normal,
MetaVideoTransformValue::ROTATED90 => Transform::Rotated90,
MetaVideoTransformValue::ROTATED180 => Transform::Rotated180,
MetaVideoTransformValue::ROTATED270 => Transform::Rotated270,
MetaVideoTransformValue::FLIPPED => Transform::Flipped,
MetaVideoTransformValue::FLIPPED90 => Transform::Flipped90,
MetaVideoTransformValue::FLIPPED180 => Transform::Flipped180,
MetaVideoTransformValue::FLIPPED270 => Transform::Flipped270,
_ => Transform::Undefined,
let mouse_meta = buffer
.find_meta::<MetaCursor>()
.filter(|c| c.is_valid())
.map(|c| MouseMeta {
x: c.position().x as f32 / format.width as f32,
y: c.position().y as f32 / format.height as f32,
});
let acquire_point = buffer
.find_meta::<MetaSyncTimeline>()
.map(|meta| meta.acquire_point);
let datas = buffer.datas_mut();
if datas.is_empty() {
log::debug!("{name}: empty PipeWire frame");
return;
}
let is_explicit = datas.len() >= 3
&& datas[datas.len() - 2].type_().as_raw() == SPA_DATA_SYNCOBJ
&& datas[datas.len() - 1].type_().as_raw() == SPA_DATA_SYNCOBJ;
if is_explicit != acquire_point.is_some() {
log::warn!("{name}: inconsistent SyncTimeline metadata / SyncObj FD layout");
return;
}
let acquire_fd = is_explicit.then(|| datas[datas.len() - 2].fd());
let datas = if is_explicit {
&datas[..datas.len() - 2]
} else {
datas
};
if datas.is_empty()
|| datas.len() > 4
|| datas
.iter()
.any(|d| d.type_().as_raw() == SPA_DATA_SYNCOBJ)
{
log::warn!("{name}: invalid PipeWire plane layout");
return;
}
let data_type = datas[0].type_();
if datas.iter().any(|d| d.type_() != data_type) {
log::warn!("{name}: mixed PipeWire data types");
return;
}
match datas[0].type_() {
DataType::DmaBuf => {
let mut planes: SmallVec<[FramePlane; 4]> = datas
.iter()
.map(|p| FramePlane {
fd: Some(p.as_raw().fd as _),
offset: p.chunk().offset(),
stride: p.chunk().stride(),
})
.collect();
let acquire_sync = if is_explicit {
let Some(device) = sync_device.as_ref() else {
log::error!("{name}: explicit sync without DRM timeline support");
return;
};
let acquire_point = acquire_point.unwrap();
let fd = acquire_fd.unwrap();
let acquire = match import_timeline(device, fd as RawFd) {
Ok(obj) => obj,
Err(e) => {
log::error!("{name}: acquire SyncObj import failed: {e}");
return;
}
};
FrameAcquireSync::Explicit {
timeline: acquire,
point: acquire_point,
}
} else {
let mut fds = Vec::with_capacity(planes.len());
for (index, plane) in planes.iter_mut().enumerate() {
let fd = plane.fd.unwrap(); // always Some
// duplicate the fd so that we can poll it for acquire sync
let duplicate = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 0) };
if duplicate < 0 {
log::error!(
"{name}: failed to duplicate DMA-buf plane {index} FD {fd}: {}",
std::io::Error::last_os_error()
);
return;
}
fds.push(unsafe { OwnedFd::from_raw_fd(duplicate) });
plane.fd = Some(duplicate);
}
FrameAcquireSync::Implicit { fds: fds.into() }
};
log::debug!("{}: Transform: {:?}", name, format.transform);
}
let mouse_meta = buffer
.find_meta::<MetaCursor>()
.filter(|c| c.is_valid())
.map(|c| MouseMeta {
x: c.position().x as f32 / format.width as f32,
y: c.position().y as f32 / format.height as f32,
let lease_id = held_buffers.borrow_mut().insert(buffer);
let tx = tx_ctrl.clone();
let lease = FrameLease::new(move || {
let _ = tx.send(PwChangeRequest::ReleaseBuffer(lease_id));
});
let datas = buffer.datas_mut();
if datas.is_empty() {
log::debug!("{}: no data", name);
return;
}
let mut dmabuf = DmabufFrame {
format: *format,
num_planes: planes.len(),
planes: Default::default(),
mouse: mouse_meta,
acquire_sync,
lease: Some(lease),
};
let planes: Vec<FramePlane> = datas
.iter()
.map(|p| FramePlane {
fd: Some(p.as_raw().fd as _),
offset: p.chunk().offset(),
stride: p.chunk().stride(),
})
.collect();
dmabuf.planes[..planes.len()].copy_from_slice(&planes[..planes.len()]);
match datas[0].type_() {
DataType::DmaBuf => {
let mut dmabuf = DmabufFrame {
format: *format,
num_planes: planes.len(),
mouse: mouse_meta,
planes: Default::default(),
};
dmabuf.planes[..planes.len()].copy_from_slice(&planes[..planes.len()]);
let frame = WlxFrame::Dmabuf(dmabuf);
if let Some(r) = receive_callback(&u, frame) {
match sender.try_send(r) {
Ok(_) => (),
Err(mpsc::TrySendError::Full(_)) => (),
Err(mpsc::TrySendError::Disconnected(_)) => {
log::warn!("{}: disconnected, stopping stream", name);
let _ = stream.disconnect();
}
if let Some(result) = receive_callback(&u, WlxFrame::Dmabuf(dmabuf)) {
match tx_frame.try_send(result) {
Ok(()) | Err(mpsc::TrySendError::Full(_)) => (),
Err(mpsc::TrySendError::Disconnected(_)) => {
log::warn!("{name}: frame receiver disconnected");
let _ = stream.disconnect();
}
}
}
DataType::MemFd => {
let memfd = MemFdFrame {
format: *format,
plane: FramePlane {
fd: Some(datas[0].as_raw().fd as _),
offset: datas[0].chunk().offset(),
stride: datas[0].chunk().stride(),
},
mouse: mouse_meta,
};
let frame = WlxFrame::MemFd(memfd);
if let Some(r) = receive_callback(&u, frame) {
match sender.try_send(r) {
Ok(_) => (),
Err(mpsc::TrySendError::Full(_)) => (),
Err(mpsc::TrySendError::Disconnected(_)) => {
log::warn!("{}: disconnected, stopping stream", name);
let _ = stream.disconnect();
}
}
}
}
DataType::MemPtr => {
let raw = datas[0].as_raw();
let chunk = datas[0].chunk();
let offset = chunk.offset() as usize;
let chunk_size = chunk.size() as usize;
let Some(size) = offset.checked_add(chunk_size) else {
log::error!("{name}: PipeWire chunk range overflow");
return;
};
if size > raw.maxsize as usize {
log::error!("{name}: PipeWire chunk extends beyond allocation");
return;
}
let memptr = MemPtrFrame {
format: *format,
ptr: raw.data as _,
size,
offset,
stride: chunk.stride() as _,
mouse: mouse_meta,
};
let frame = WlxFrame::MemPtr(memptr);
if let Some(r) = receive_callback(&u, frame) {
match sender.try_send(r) {
Ok(_) => (),
Err(mpsc::TrySendError::Full(_)) => (),
Err(mpsc::TrySendError::Disconnected(_)) => {
log::warn!("{}: disconnected, stopping stream", name);
let _ = stream.disconnect();
}
}
}
}
_ => {
log::error!("Received invalid frame data type ({:?})", datas[0].type_())
}
}
DataType::MemFd => {
let memfd = MemFdFrame {
format: *format,
plane: FramePlane {
fd: Some(datas[0].as_raw().fd as _),
offset: datas[0].chunk().offset(),
stride: datas[0].chunk().stride(),
},
mouse: mouse_meta,
};
let frame = WlxFrame::MemFd(memfd);
if let Some(r) = receive_callback(&u, frame) {
match tx_frame.try_send(r) {
Ok(_) => (),
Err(mpsc::TrySendError::Full(_)) => (),
Err(mpsc::TrySendError::Disconnected(_)) => {
log::warn!("{}: disconnected, stopping stream", name);
let _ = stream.disconnect();
}
}
}
}
DataType::MemPtr => {
let raw = datas[0].as_raw();
let chunk = datas[0].chunk();
let offset = chunk.offset() as usize;
let chunk_size = chunk.size() as usize;
let Some(size) = offset.checked_add(chunk_size) else {
log::error!("{name}: PipeWire chunk range overflow");
return;
};
if size > raw.maxsize as usize {
log::error!("{name}: PipeWire chunk extends beyond allocation");
return;
}
let memptr = MemPtrFrame {
format: *format,
ptr: raw.data as _,
size,
offset,
stride: chunk.stride() as _,
mouse: mouse_meta,
};
let frame = WlxFrame::MemPtr(memptr);
if let Some(r) = receive_callback(&u, frame) {
match tx_frame.try_send(r) {
Ok(_) => (),
Err(mpsc::TrySendError::Full(_)) => (),
Err(mpsc::TrySendError::Disconnected(_)) => {
log::warn!("{}: disconnected, stopping stream", name);
let _ = stream.disconnect();
}
}
}
}
_ => log::error!("Received invalid frame data type ({:?})", datas[0].type_()),
}
// DMA-buf frames transfer ownership to held_buffers,
// requeued when their lease expires
}
})
.register()?;
@@ -448,9 +572,10 @@ where
params.as_mut_slice(),
)?;
let _receiver = receiver.attach(main_loop.loop_(), {
let _receiver = rx_ctrl.attach(main_loop.loop_(), {
let name = name.clone();
let main_loop = main_loop.clone();
let held_buffers = held_buffers.clone();
move |req| {
log::debug!("{name}: request pipewire stream to {req:?}");
@@ -465,6 +590,9 @@ where
.set_active(true)
.inspect_err(|e| log::warn!("Could not {req:?} pipewire stream: {e:?}"));
}
PwChangeRequest::ReleaseBuffer(id) => {
held_buffers.borrow_mut().requeue(&stream, id);
}
PwChangeRequest::Stop => {
main_loop.quit();
}
@@ -491,7 +619,25 @@ fn get_buffer_params() -> Object {
| (1 << DataType::MemPtr.as_raw())
| (1 << DataType::DmaBuf.as_raw());
let property = Property {
get_buffer_params_with_data_types(data_types)
}
fn get_buffer_params_with_data_types(data_types: i32) -> Object {
// since we hold onto buffers until rendering is done,
// fewer than 4 buffers may produce stalls
let buffers = Property {
key: spa::sys::SPA_PARAM_BUFFERS_buffers,
flags: PropertyFlags::empty(),
value: Value::Choice(ChoiceValue::Int(Choice(
ChoiceFlags::empty(),
ChoiceEnum::Range {
default: 6,
min: 4,
max: MAX_PW_BUFFERS,
},
))),
};
let data_types = Property {
key: spa::sys::SPA_PARAM_BUFFERS_dataType,
flags: PropertyFlags::empty(),
value: Value::Int(data_types),
@@ -500,10 +646,35 @@ fn get_buffer_params() -> Object {
spa::pod::object!(
spa::utils::SpaTypes::ObjectParamBuffers,
spa::param::ParamType::Buffers,
property,
buffers,
data_types,
)
}
fn get_sync_buffer_params() -> Object {
let mut obj = get_buffer_params_with_data_types(1 << DataType::DmaBuf.as_raw());
obj.properties.push(Property {
key: spa::sys::SPA_PARAM_BUFFERS_blocks,
flags: PropertyFlags::empty(),
value: Value::Choice(ChoiceValue::Int(Choice(
ChoiceFlags::empty(),
ChoiceEnum::Range {
default: 3,
// when using sync timeline, we want at least 1 image plane + 2 syncobj fds
min: 3,
// up to 4 image planes
max: 6,
},
))),
});
obj.properties.push(Property {
key: SPA_PARAM_BUFFERS_META_TYPE,
flags: PropertyFlags::MANDATORY,
value: Value::Int(1_i32 << SPA_META_SYNC_TIMELINE),
});
obj
}
fn get_meta_object(key: u32, size: usize) -> Object {
let meta_type_property = Property {
key: spa::sys::SPA_PARAM_META_type,
@@ -641,3 +812,15 @@ fn spa_to_fourcc(spa: VideoFormat) -> DrmFourcc {
_ => panic!("Unsupported format"),
}
}
#[repr(C)]
struct MetaSyncTimeline {
_flags: u32,
_padding: u32,
acquire_point: u64,
_release_point: u64,
}
impl spa::buffer::meta::Metadata for MetaSyncTimeline {
const META_TYPE: u32 = SPA_META_SYNC_TIMELINE;
}
+1
View File
@@ -26,6 +26,7 @@ slotmap::new_key_type! {
pub mod capture;
mod dbus_screencast;
mod raw_buffer;
#[derive(Debug, Clone)]
pub struct PipewireStream {
+123
View File
@@ -0,0 +1,123 @@
use std::{marker::PhantomData, mem, ptr::NonNull};
use pipewire::{
self as pw,
spa::{self, buffer::meta::Metadata},
stream::Stream,
};
pub(super) struct DequeuedBuffer<'a> {
stream: &'a Stream,
ptr: NonNull<pw::sys::pw_buffer>,
_not_send: PhantomData<std::rc::Rc<()>>,
}
impl<'a> DequeuedBuffer<'a> {
pub(super) fn dequeue(stream: &'a Stream) -> Option<Self> {
let ptr = NonNull::new(unsafe { stream.dequeue_raw_buffer() })?;
Some(Self {
stream,
ptr,
_not_send: PhantomData,
})
}
fn spa(&self) -> Option<NonNull<spa::sys::spa_buffer>> {
NonNull::new(unsafe { self.ptr.read().buffer })
}
pub(super) fn datas_mut(&self) -> &'a mut [pw::spa::buffer::Data] {
let Some(buffer) = self.spa() else {
return &mut [];
};
unsafe {
let n_datas = buffer.read().n_datas;
if n_datas > 0
&& let Some(datas) = NonNull::new(buffer.read().datas)
{
let datas = datas.as_ptr() as *mut pw::spa::buffer::Data;
std::slice::from_raw_parts_mut(datas, usize::try_from(n_datas).unwrap())
} else {
&mut []
}
}
}
pub(super) fn find_meta<T>(&self) -> Option<&'a T>
where
T: Metadata,
{
let buffer = self.spa()?;
unsafe {
if buffer.read().n_metas != 0 {
match T::META_TYPE {
pw::spa::sys::SPA_META_VideoDamage => {
let meta_data =
pw::spa::sys::spa_buffer_find_meta(buffer.as_ptr(), T::META_TYPE)
as *const T;
if !meta_data.is_null() {
return Some(&*meta_data);
}
}
_ => {
let meta_data = pw::spa::sys::spa_buffer_find_meta_data(
buffer.as_ptr(),
T::META_TYPE,
std::mem::size_of::<T>(),
) as *const T;
if !meta_data.is_null() {
return Some(&*meta_data);
}
}
}
}
}
None
}
}
pub(super) const MAX_PW_BUFFERS: i32 = 8;
impl Drop for DequeuedBuffer<'_> {
fn drop(&mut self) {
unsafe { self.stream.queue_raw_buffer(self.ptr.as_ptr()) };
}
}
#[derive(Default)]
pub(super) struct HeldBuffers {
buffers: Vec<NonNull<pw::sys::pw_buffer>>,
}
impl HeldBuffers {
pub(super) fn insert<'a>(&mut self, buffer: DequeuedBuffer<'a>) -> usize {
self.buffers.push(buffer.ptr);
let ret_val = buffer.ptr.as_ptr() as usize;
mem::forget(buffer);
ret_val
}
pub(super) fn on_removed(&mut self, buffer: NonNull<pw::sys::pw_buffer>) -> bool {
let before_count = self.buffers.len();
self.buffers.retain(|ptr| ptr.as_ptr() != buffer.as_ptr());
before_count != self.buffers.len()
}
pub(super) fn requeue(&mut self, stream: &pw::stream::Stream, id: usize) {
self.buffers.retain(|ptr| {
if ptr.as_ptr() as usize == id {
unsafe { stream.queue_raw_buffer(ptr.as_ptr()) };
false
} else {
true
}
});
}
}
impl Drop for HeldBuffers {
fn drop(&mut self) {
log::warn!("{} checked-out buffers at shutdown", self.buffers.len());
self.buffers.clear();
}
}
+88
View File
@@ -0,0 +1,88 @@
use std::{
fs, io,
os::{
fd::{AsFd, AsRawFd, BorrowedFd, RawFd},
unix::fs::{FileTypeExt, MetadataExt},
},
sync::Arc,
};
pub use timeline_syncobj::render_node::DrmRenderNode;
use timeline_syncobj::timeline_syncobj::TimelineSyncObj;
const DRM_CAP_SYNCOBJ: u64 = 0x13;
const DRM_CAP_SYNCOBJ_TIMELINE: u64 = 0x14;
#[repr(C)]
struct DrmGetCap {
capability: u64,
value: u64,
}
const DRM_IOCTL_GET_CAP: libc::c_ulong = ((3_u64 << 30)
| ((std::mem::size_of::<DrmGetCap>() as u64) << 16)
| ((b'd' as u64) << 8)
| 0x0c) as libc::c_ulong;
fn get_cap(node: &DrmRenderNode, capability: u64) -> io::Result<u64> {
let mut arg = DrmGetCap {
capability,
value: 0,
};
let ret = unsafe { libc::ioctl(node.as_fd().as_raw_fd(), DRM_IOCTL_GET_CAP, &mut arg) };
if ret < 0 {
Err(io::Error::last_os_error())
} else {
Ok(arg.value)
}
}
fn supports_timeline(node: &DrmRenderNode) -> bool {
[DRM_CAP_SYNCOBJ, DRM_CAP_SYNCOBJ_TIMELINE]
.into_iter()
.all(|cap| get_cap(node, cap).is_ok_and(|value| value != 0))
}
pub fn open_render_node(render_major: u32, render_minor: u32) -> io::Result<DrmRenderNode> {
if !(128..=255).contains(&render_minor) {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
format!("Unsupported DRM render minor: {render_minor}"),
));
}
let path = format!("/dev/dri/renderD{render_minor}");
let metadata = fs::metadata(&path)?;
// verify that device actually matches the provided minor/major
if !metadata.file_type().is_char_device()
|| metadata.rdev() != libc::makedev(render_major, render_minor)
{
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("{path} does not match DRM device {render_major}:{render_minor}"),
));
}
let node = DrmRenderNode::new(render_minor.into())
.map_err(|e| io::Error::from_raw_os_error(e.raw_os_error()))?;
if !supports_timeline(&node) {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
format!("{path} does not support DRM syncobj timelines"),
));
}
Ok(node)
}
pub(crate) fn import_timeline(node: &DrmRenderNode, fd: RawFd) -> io::Result<Arc<TimelineSyncObj>> {
if fd < 0 {
return Err(io::Error::from_raw_os_error(libc::EBADF));
}
let fd = unsafe { BorrowedFd::borrow_raw(fd) };
TimelineSyncObj::import(node, fd)
.map(Arc::new)
.map_err(|e| io::Error::from_raw_os_error(e.raw_os_error()))
}
+1 -18
View File
@@ -11,10 +11,7 @@ use smithay_client_toolkit::reexports::{
zxdg_output_manager_v1::ZxdgOutputManagerV1,
zxdg_output_v1::{self, ZxdgOutputV1},
},
protocols_wlr::{
export_dmabuf::v1::client::zwlr_export_dmabuf_manager_v1::ZwlrExportDmabufManagerV1,
screencopy::v1::client::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1,
},
protocols_wlr::screencopy::v1::client::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1,
};
use wayland_client::{
@@ -61,7 +58,6 @@ pub struct WlxOutput {
pub struct WlxClient {
pub connection: Arc<Connection>,
pub xdg_output_mgr: ZxdgOutputManagerV1,
pub maybe_wlr_dmabuf_mgr: Option<ZwlrExportDmabufManagerV1>,
pub maybe_wlr_screencopy_mgr: Option<ZwlrScreencopyManagerV1>,
pub maybe_zwp_linux_dmabuf: Option<ZwpLinuxDmabufV1>,
pub wl_seat: WlSeat,
@@ -93,7 +89,6 @@ impl WlxClient {
.bind(&qh, 4..=9, ())
.expect(WlSeat::interface().name),
wl_shm: globals.bind(&qh, 1..=1, ()).expect(WlShm::interface().name),
maybe_wlr_dmabuf_mgr: globals.bind(&qh, 1..=1, ()).ok(),
maybe_wlr_screencopy_mgr: globals.bind(&qh, 3..=3, ()).ok(),
maybe_zwp_linux_dmabuf: globals.bind(&qh, 4..=4, ()).ok(),
outputs: IdMap::new(),
@@ -398,18 +393,6 @@ impl Dispatch<ZxdgOutputManagerV1, ()> for WlxClient {
}
}
impl Dispatch<ZwlrExportDmabufManagerV1, ()> for WlxClient {
fn event(
_state: &mut Self,
_proxy: &ZwlrExportDmabufManagerV1,
_event: <ZwlrExportDmabufManagerV1 as Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
}
}
impl Dispatch<ZwlrScreencopyManagerV1, ()> for WlxClient {
fn event(
_state: &mut Self,
-258
View File
@@ -1,258 +0,0 @@
use std::{
any::Any,
collections::VecDeque,
os::fd::{FromRawFd, IntoRawFd, OwnedFd, RawFd},
sync::mpsc,
thread::JoinHandle,
};
use drm_fourcc::{DrmFormat, DrmFourcc, DrmModifier};
use smithay_client_toolkit::reexports::protocols_wlr::export_dmabuf::v1::client::zwlr_export_dmabuf_frame_v1::{self, ZwlrExportDmabufFrameV1};
use wayland_client::{Connection, QueueHandle, Dispatch, Proxy};
use crate::{
WlxCapture,
frame::{DmabufFrame, FrameFormat, FramePlane, WlxFrame},
wayland::WlxClient,
};
use log::{debug, warn};
struct CaptureData<U, R>
where
U: Any,
R: Any,
{
sender: mpsc::SyncSender<WlxFrame>,
receiver: mpsc::Receiver<WlxFrame>,
user_data: U,
receive_callback: fn(&U, WlxFrame) -> Option<R>,
}
pub struct WlrDmabufCapture<U, R>
where
U: Any + Send,
R: Any + Send,
{
output_id: u32,
wl: Option<Box<WlxClient>>,
handle: Option<JoinHandle<Box<WlxClient>>>,
data: Option<CaptureData<U, R>>,
fds: VecDeque<RawFd>,
}
impl<U, R> WlrDmabufCapture<U, R>
where
U: Any + Send,
R: Any + Send,
{
pub fn new(wl: WlxClient, output_id: u32) -> Self {
Self {
output_id,
wl: Some(Box::new(wl)),
handle: None,
data: None,
fds: VecDeque::new(),
}
}
}
impl<U, R> WlxCapture<U, R> for WlrDmabufCapture<U, R>
where
U: Any + Send,
R: Any + Send,
{
fn init(
&mut self,
_: &[DrmFormat],
user_data: U,
receive_callback: fn(&U, WlxFrame) -> Option<R>,
) {
debug_assert!(self.wl.is_some());
let (sender, receiver) = std::sync::mpsc::sync_channel::<WlxFrame>(2);
self.data = Some(CaptureData {
sender,
receiver,
user_data,
receive_callback,
});
}
fn is_ready(&self) -> bool {
self.data.is_some()
}
fn supports_dmbuf(&self) -> bool {
true
}
fn receive(&mut self) -> Option<R> {
if let Some(data) = self.data.as_ref()
&& let Some(WlxFrame::Dmabuf(last)) = data.receiver.try_iter().last()
{
// this is the only protocol that requires us to manually close the FD
while self.fds.len() > 6 * last.num_planes {
// safe unwrap
let _ = unsafe { OwnedFd::from_raw_fd(self.fds.pop_back().unwrap()) };
}
for p in 0..last.num_planes {
if let Some(fd) = last.planes[p].fd {
self.fds.push_front(fd);
}
}
return (data.receive_callback)(&data.user_data, WlxFrame::Dmabuf(last));
}
None
}
fn pause(&mut self) {}
fn resume(&mut self) {
self.receive(); // clear old frames
}
fn request_new_frame(&mut self) {
if let Some(handle) = self.handle.take() {
if handle.is_finished() {
self.wl = Some(handle.join().unwrap()); // safe to unwrap because we checked is_finished
} else {
self.handle = Some(handle);
return;
}
}
let Some(wl) = self.wl.take() else {
return;
};
self.handle = Some(std::thread::spawn({
let sender = self
.data
.as_ref()
.expect("must call init once before request_new_frame")
.sender
.clone();
let output_id = self.output_id;
move || request_dmabuf_frame(wl, output_id, sender)
}));
}
}
/// Request a new DMA-Buf frame using the wlr-export-dmabuf protocol.
fn request_dmabuf_frame(
client: Box<WlxClient>,
output_id: u32,
sender: mpsc::SyncSender<WlxFrame>,
) -> Box<WlxClient> {
let Some(dmabuf_manager) = client.maybe_wlr_dmabuf_mgr.as_ref() else {
return client;
};
let Some(output) = client.outputs.get(output_id) else {
return client;
};
let transform = output.transform;
let (tx, rx) = mpsc::sync_channel::<zwlr_export_dmabuf_frame_v1::Event>(16);
let name = output.name.clone();
let _ = dmabuf_manager.capture_output(1, &output.wl_output, &client.queue_handle, tx.clone());
let mut client = client;
client.dispatch();
let mut frame = None;
rx.try_iter().for_each(|event| match event {
zwlr_export_dmabuf_frame_v1::Event::Frame {
width,
height,
format,
mod_high,
mod_low,
num_objects,
..
} => {
let mut new_frame = DmabufFrame {
format: FrameFormat {
width,
height,
drm_format: DrmFormat {
code: match DrmFourcc::try_from(format) {
Ok(code) => code,
Err(e) => {
log::error!("Unrecognized fourcc: {e:?}");
return;
}
},
modifier: DrmModifier::Invalid,
},
transform,
},
mouse: None,
num_planes: num_objects as _,
planes: Default::default(),
};
new_frame.format.set_mod(mod_high, mod_low);
frame = Some(new_frame);
}
zwlr_export_dmabuf_frame_v1::Event::Object {
index,
fd,
offset,
stride,
..
} => {
let Some(ref mut frame) = frame else {
return;
};
frame.planes[index as usize] = FramePlane {
fd: Some(fd.into_raw_fd()),
offset,
stride: stride as _,
};
}
zwlr_export_dmabuf_frame_v1::Event::Ready { .. } => {
let Some(frame) = frame.take() else {
return;
};
debug!("DMA-Buf frame captured");
let frame = WlxFrame::Dmabuf(frame);
match sender.try_send(frame) {
Ok(_) => (),
Err(mpsc::TrySendError::Full(_)) => (),
Err(mpsc::TrySendError::Disconnected(_)) => {
log::warn!("{}: disconnected", name);
}
}
}
zwlr_export_dmabuf_frame_v1::Event::Cancel { .. } => {
warn!("DMA-Buf frame capture cancelled");
}
_ => {}
});
client
}
impl Dispatch<ZwlrExportDmabufFrameV1, mpsc::SyncSender<zwlr_export_dmabuf_frame_v1::Event>>
for WlxClient
{
fn event(
_state: &mut Self,
proxy: &ZwlrExportDmabufFrameV1,
event: <ZwlrExportDmabufFrameV1 as Proxy>::Event,
data: &mpsc::SyncSender<zwlr_export_dmabuf_frame_v1::Event>,
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
match event {
zwlr_export_dmabuf_frame_v1::Event::Ready { .. }
| zwlr_export_dmabuf_frame_v1::Event::Cancel { .. } => {
proxy.destroy();
}
_ => {}
}
let _ = data.send(event).or_else(|err| {
warn!("Failed to send DMA-Buf frame event: {}", err);
Ok::<(), mpsc::SendError<zwlr_export_dmabuf_frame_v1::Event>>(())
});
}
}
+4 -1
View File
@@ -23,6 +23,8 @@ pub enum Language {
ChineseSimplified,
#[strum(props(Text = "Español"))]
Spanish,
#[strum(props(Text = "Svenska"))]
Swedish,
}
impl Language {
@@ -36,6 +38,7 @@ impl Language {
Language::Italian => "it_IT",
Language::ChineseSimplified => "zh_CN",
Language::Spanish => "es_ES",
Language::Swedish => "sv_SE",
}
}
@@ -44,7 +47,7 @@ impl Language {
}
pub const fn all_codes() -> &'static [&'static str] {
&["en", "pl", "ja", "de", "it", "zh_CN", "es"]
&["en", "pl", "ja", "de", "it", "zh_CN", "es", "sv"]
}
}