diff --git a/app/src/main/cpp/waylandcomp/src/compositor.c b/app/src/main/cpp/waylandcomp/src/compositor.c index 6f50a889b..890d14913 100644 --- a/app/src/main/cpp/waylandcomp/src/compositor.c +++ b/app/src/main/cpp/waylandcomp/src/compositor.c @@ -1523,7 +1523,7 @@ static void bind_output(struct wl_client *c, void *data, uint32_t ver, uint32_t wl_list_insert(&g_outputs, wl_resource_get_link(r)); wl_output_send_geometry(r, 0, 0, 340, 190, WL_OUTPUT_SUBPIXEL_UNKNOWN, "DroidDeck", "Wayland", WL_OUTPUT_TRANSFORM_NORMAL); - output_send_mode(r, atomic_load(&g_output_refresh_mhz)); + output_send_mode(r, g_output_announced_refresh_mhz); if (ver >= WL_OUTPUT_NAME_SINCE_VERSION) { wl_output_send_name(r, "DroidDeck-1"); wl_output_send_description(r, "DroidDeck display"); diff --git a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt index 48c43aa12..60527f1ff 100644 --- a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt @@ -1283,7 +1283,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } if (!CompositorHost.isStarted) { val mode = SessionPrefs.prefMode(intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM) - val probe = HdrSupport.probe(this) + val probe = HdrSupport.probe(this, glassesScreen?.display) WaylandCompositor.nativeSetHdrDisplay( probe.displayId, probe.name, probe.formats, probe.hdr10, probe.maxLuminance, probe.maxAverageLuminance, probe.minLuminance, diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/Glasses.kt b/app/src/main/java/com/droiddeck/launcher/glasses/Glasses.kt index 86f363f5a..8e859d554 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/Glasses.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/Glasses.kt @@ -97,7 +97,7 @@ object Glasses { private const val TAG = "Glasses" private const val ACTION_PERMISSION = "com.droiddeck.launcher.glasses.USB_PERMISSION" private const val PREFS = "glasses" - private const val KEY_SBS_SUPPORT = "sbsSupport" + private const val KEY_SBS_FAILED = "sbsFailed" private const val SBS_CONFIRM_MS = 20_000L private val _state = MutableStateFlow(GlassesState()) @@ -133,7 +133,6 @@ object Glasses { usb = app.getSystemService(Context.USB_SERVICE) as UsbManager displays = app.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager _settings.value = load() - app.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().remove(KEY_SBS_SUPPORT).apply() handler = Handler(HandlerThread("droiddeck-glasses").apply { start() }.looper) handler.post { val filter = IntentFilter(ACTION_PERMISSION).apply { @@ -180,7 +179,10 @@ object Glasses { modeState.manual() val s = _state.value if (!s.canControl || (on && s.noSignal)) return@post - if (on) rememberSbs(GlassesState.SBS_UNKNOWN) + if (on) { + markSbsFailed(linkDevice?.productId, false) + rememberSbs(GlassesState.SBS_UNKNOWN) + } sendMode(on) } @@ -213,7 +215,11 @@ object Glasses { sendFilm(on) } - fun setSbsSupport(support: Int) = post { rememberSbs(support.coerceIn(GlassesState.SBS_FAILS, GlassesState.SBS_WORKS)) } + fun setSbsSupport(support: Int) = post { + val s = support.coerceIn(GlassesState.SBS_FAILS, GlassesState.SBS_WORKS) + markSbsFailed(linkDevice?.productId, s == GlassesState.SBS_FAILS) + rememberSbs(s) + } fun setGameOnGlasses(on: Boolean) = post { update { it.copy(gameOnGlasses = on) } } fun setHeadTracking(on: Boolean) = post { update { it.copy(headTracking = on) } } fun setAuto3d(on: Boolean) = post { update { it.copy(auto3d = on) } } @@ -350,7 +356,7 @@ object Glasses { GlassesModeState.Device(it.productId, if (permitted) runCatching { it.serialNumber }.getOrNull() else null) } if (modeState.updateDevice(identity, SystemClock.uptimeMillis())) { - rememberSbs(GlassesState.SBS_UNKNOWN) + rememberSbs(if (identity != null && sbsFailed(identity.productId)) GlassesState.SBS_FAILS else GlassesState.SBS_UNKNOWN) } if (device == null || (linkDevice != null && linkDevice?.deviceName != device.deviceName)) closeLink() model = m @@ -363,16 +369,20 @@ object Glasses { when (modeState.observe(sbsByDisplay, SystemClock.uptimeMillis())) { GlassesModeState.Confirmation.FAILED -> { Log.w(TAG, "3D signal was not confirmed at ${width}x$height; restoring 2D") + if (noSignal) markSbsFailed(device?.productId, true) rememberSbs(GlassesState.SBS_FAILS) } else -> Unit } - if (sbsByDisplay) rememberSbs(GlassesState.SBS_WORKS) + if (sbsByDisplay) { + markSbsFailed(device?.productId, false) + rememberSbs(GlassesState.SBS_WORKS) + } if (controllable && device != null && !permitted && (ask || !asked) && _state.value.permission != Permission.ASKING) askPermission(device) if (device != null && permitted && link == null) openLink(device) publish { it.copy( - present = devices.isNotEmpty() || display != null, + present = device != null || display != null, model = m?.name ?: display?.let(::displayName) ?: "VITURE", controllable = controllable, permission = when { @@ -464,6 +474,17 @@ object Glasses { publish { it.copy(sbsSupport = support) } } + private fun sbsFailed(productId: Int): Boolean = + app.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getStringSet(KEY_SBS_FAILED, null)?.contains(productId.toString(16)) == true + + private fun markSbsFailed(productId: Int?, failed: Boolean) { + if (productId == null || sbsFailed(productId) == failed) return + val prefs = app.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + val ids = prefs.getStringSet(KEY_SBS_FAILED, null).orEmpty() + val id = productId.toString(16) + prefs.edit().putStringSet(KEY_SBS_FAILED, if (failed) ids + id else ids - id).apply() + } + private fun applyMode(force: Boolean) { val s = _state.value val m = model ?: return @@ -521,9 +542,9 @@ object Glasses { private fun linkListener(generation: Long) = object : VitureLink.Listener { override fun onFrame(frame: VitureProtocol.Frame) = post { if (generation != linkGeneration) return@post - val line = "%s 0x%04X %s".format( + val line = "%s 0x%04X %s%s".format( when (frame.magic) { VitureProtocol.MAGIC_REPORT -> "FD"; VitureProtocol.MAGIC_COMMAND -> "FE"; else -> "FC" }, - frame.msgId, frame.payload.joinToString("") { "%02X".format(it) }, + frame.msgId, frame.payload.joinToString("") { "%02X".format(it) }, if (frame.crcOk) "" else " crc!", ) synchronized(frames) { frames.addLast(line) diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/VitureProtocol.kt b/app/src/main/java/com/droiddeck/launcher/glasses/VitureProtocol.kt index 1b943b284..cfe830b90 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/VitureProtocol.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/VitureProtocol.kt @@ -85,14 +85,6 @@ object VitureProtocol { return Model(name, family) } - fun modeIsSbs(mode: Int): Boolean = mode == 0x32 || mode == 0x35 || mode == 0x42 || mode == 0x45 - - fun modeHz(mode: Int): Int = when (mode) { - 0x34, 0x44 -> 120 - 0x33, 0x35, 0x43, 0x45 -> 90 - else -> 60 - } - private val CRC_TABLE = IntArray(256) { i -> var c = i shl 8 repeat(8) { c = if (c and 0x8000 != 0) (c shl 1) xor 0x1021 else c shl 1 } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/GlassesPanel.kt b/app/src/main/java/com/droiddeck/launcher/ui/GlassesPanel.kt index aa19c6fb8..2cfe79b97 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/GlassesPanel.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/GlassesPanel.kt @@ -92,11 +92,13 @@ internal fun GlassesGroups( stringResource(if (g.sbs) R.string.glasses_mode_3d else R.string.glasses_mode_2d)) else null when { g.noSignal -> SettingsRow(stringResource(R.string.glasses_output), stringResource(R.string.glasses_no_signal)) {} - !g.controllable && g.present && g.permission == Glasses.Permission.NONE && !g.connected -> - SettingsRow(stringResource(R.string.glasses_control), status ?: stringResource(R.string.glasses_no_control)) {} + !g.controllable -> { + if (status != null) SettingsRow(stringResource(R.string.glasses_output), status) {} + SettingsRow(stringResource(R.string.glasses_control), stringResource(R.string.glasses_no_control)) {} + } g.permission == Glasses.Permission.ASKING -> SettingsRow(stringResource(R.string.glasses_control), stringResource(R.string.glasses_asking)) {} - !g.connected && g.controllable -> ActionRow(stringResource(R.string.glasses_control), + !g.connected -> ActionRow(stringResource(R.string.glasses_control), stringResource(if (g.permission == Glasses.Permission.DENIED) R.string.glasses_denied else R.string.glasses_allow_hint), stringResource(R.string.glasses_allow), { Glasses.requestControl() }) status != null -> SettingsRow(stringResource(R.string.glasses_output), status) {} @@ -115,9 +117,9 @@ internal fun GlassesGroups( g.sbs, chipModifier = track("glasses-3d")) { Glasses.set3d(it) } if (g.sbs90) ChoiceRow(host, "glasses-3d-hz", stringResource(R.string.glasses_3d_refresh), null, listOf(60, 90).map { it to "$it Hz" }, s.sbsHz, chipModifier = track("glasses-3d-hz")) { Glasses.setSbsHz(it) } - ToggleRow(host, "glasses-auto3d", stringResource(R.string.glasses_auto3d), stringResource(R.string.glasses_auto3d_hint), - s.auto3d, chipModifier = track("glasses-auto3d")) { Glasses.setAuto3d(it) } } + ToggleRow(host, "glasses-auto3d", stringResource(R.string.glasses_auto3d), stringResource(R.string.glasses_auto3d_hint), + s.auto3d, chipModifier = track("glasses-auto3d")) { Glasses.setAuto3d(it) } ChoiceRow(host, "glasses-picture", stringResource(R.string.glasses_picture), stringResource(R.string.glasses_picture_hint), listOf( GlassesSettings.PICTURE_AUTO to stringResource(R.string.glasses_picture_auto), diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SteamHome.kt b/app/src/main/java/com/droiddeck/launcher/ui/SteamHome.kt index 73806d42c..ef8517cec 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SteamHome.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SteamHome.kt @@ -104,7 +104,7 @@ internal fun SteamHome(s: FrontEndState, a: FrontEndActions, modifier: Modifier) if (glasses.present) DropSheet( drop, glasses.model, GlassesIcon, glassesTabs(glasses), covered = lifted, beside = { enabled -> - PrimaryButton(stringResource(R.string.games_play_steamvr), enabled = !s.busy, large = true, icon = Icons.Filled.PlayArrow, fill = true, + if (glasses.canControl) PrimaryButton(stringResource(R.string.games_play_steamvr), enabled = !s.busy, large = true, icon = Icons.Filled.PlayArrow, fill = true, modifier = Modifier.fillMaxWidth().testTag("play-steamvr")) { if (enabled) a.onPlaySteamVr() } }, ) { tab -> diff --git a/app/src/main/java/com/droiddeck/launcher/wayland/HdrSupport.kt b/app/src/main/java/com/droiddeck/launcher/wayland/HdrSupport.kt index 605a1579d..91f408d91 100644 --- a/app/src/main/java/com/droiddeck/launcher/wayland/HdrSupport.kt +++ b/app/src/main/java/com/droiddeck/launcher/wayland/HdrSupport.kt @@ -20,8 +20,8 @@ class HdrProbe( ) object HdrSupport { - fun probe(context: Context): HdrProbe { - val display: Display? = if (Build.VERSION.SDK_INT >= 30) { + fun probe(context: Context, output: Display? = null): HdrProbe { + val display: Display? = output ?: if (Build.VERSION.SDK_INT >= 30) { try { context.display } catch (e: Exception) { null } } else { @Suppress("DEPRECATION") diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 54a446c3e..f0431a545 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -2313,7 +2313,6 @@ معدل التحديث 3D (جنبًا إلى جنب) تحصل كل عين على نصفها الخاص من صورة بدقة 3840×1080. إذا انقطعت الصورة، فاضغط مطولًا على زر 2D/3D في النظارة. - لا يمكن لهذا الهاتف إرسال إشارة 3840×1080 التي يحتاجها وضع 3D. تعرض ألعاب الواقع الافتراضي منظور عين واحدة، مع تتبّع الرأس. لم يتم تأكيد إشارة 3D. حاول مجددًا بعد عودة صورة 2D. لا توجد صورة: النظارة في وضع لا يستطيع هذا الهاتف إرساله. اضغط مطولًا على زر 2D/3D فيها، أو افصلها ثم أعد توصيلها. معدل تحديث 3D diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 6a4eaf67e..5ee69c757 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -2213,7 +2213,6 @@ Честота на опресняване 3D (един до друг) Всяко око получава своята половина от картина 3840×1080. Ако картината изчезне, задръжте бутона 2D/3D на очилата. - Този телефон не може да изпрати сигнала 3840×1080, нужен за 3D. VR игрите показват изгледа за едно око, с проследяване на главата. 3D сигналът не беше потвърден. Опитайте отново, когато 2D картината се върне. Няма картина: очилата са в режим, който този телефон не може да изпрати. Задръжте бутона им 2D/3D или ги изключете и включете отново. Честота на опресняване в 3D diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 1a021f707..14a15492b 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -2263,7 +2263,6 @@ Obnovovací frekvence 3D (vedle sebe) Každé oko dostane vlastní polovinu obrazu 3840×1080. Pokud obraz vypadne, podržte na brýlích tlačítko 2D/3D. - Tento telefon neumí vyslat signál 3840×1080, který 3D vyžaduje. Hry VR zobrazí pohled jednoho oka se sledováním hlavy. Signál 3D se nepodařilo potvrdit. Zkuste to znovu, až se vrátí obraz 2D. Žádný obraz: brýle jsou v režimu, který tento telefon neumí vyslat. Podržte jejich tlačítko 2D/3D, nebo je odpojte a znovu připojte. Obnovovací frekvence ve 3D diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 097a2f84a..064116c9c 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -2213,7 +2213,6 @@ Opdateringshastighed 3D (side-by-side) Hvert øje får sin egen halvdel af et billede på 3840×1080. Forsvinder billedet, så hold brillens 2D/3D-knap inde. - Denne telefon kan ikke sende det 3840×1080-signal, som 3D kræver. VR-spil viser ét øjes billede med hovedsporing. 3D-signalet blev ikke bekræftet. Prøv igen, når 2D-billedet er tilbage. Intet billede: brillen er i en tilstand, som telefonen ikke kan sende. Hold dens 2D/3D-knap inde, eller tag stikket ud og sæt det i igen. 3D-opdateringshastighed diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 2562c33d8..12876c794 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -2213,7 +2213,6 @@ Bildwiederholrate 3D (Side-by-Side) Jedes Auge bekommt seine eigene Hälfte eines 3840×1080-Bildes. Fällt das Bild aus, halte die 2D/3D-Taste der Brille gedrückt. - Dieses Smartphone kann das 3840×1080-Signal für 3D nicht ausgeben. VR-Spiele zeigen die Sicht eines Auges, mit Kopf-Tracking. Das 3D-Signal wurde nicht bestätigt. Versuche es erneut, sobald das 2D-Bild wieder da ist. Kein Bild: Die Brille ist in einem Modus, den dieses Smartphone nicht ausgeben kann. Halte ihre 2D/3D-Taste gedrückt oder steck sie aus und wieder ein. 3D-Bildwiederholrate diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 3a497b6b0..9b83f3a78 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -2213,7 +2213,6 @@ Ρυθμός ανανέωσης 3D (δίπλα-δίπλα) Κάθε μάτι παίρνει το δικό του μισό μιας εικόνας 3840×1080. Αν η εικόνα χαθεί, κρατήστε πατημένο το κουμπί 2D/3D των γυαλιών. - Αυτό το τηλέφωνο δεν μπορεί να στείλει το σήμα 3840×1080 που χρειάζεται το 3D. Τα παιχνίδια VR δείχνουν την εικόνα του ενός ματιού, με παρακολούθηση κεφαλιού. Το σήμα 3D δεν επιβεβαιώθηκε. Δοκιμάστε ξανά μόλις επανέλθει η εικόνα 2D. Δεν υπάρχει εικόνα: τα γυαλιά είναι σε λειτουργία που αυτό το τηλέφωνο δεν μπορεί να στείλει. Κρατήστε πατημένο το κουμπί 2D/3D τους ή αποσυνδέστε τα και συνδέστε τα ξανά. Ρυθμός ανανέωσης 3D diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 495e1fa32..1f6a020a7 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -2246,7 +2246,6 @@ Frecuencia de actualización 3D (lado a lado) Cada ojo recibe su propia mitad de una imagen de 3840×1080. Si la imagen se pierde, mantén pulsado el botón 2D/3D de las gafas. - Este teléfono no puede enviar la señal de 3840×1080 que necesita el 3D. Los juegos de RV muestran la vista de un ojo, con seguimiento de cabeza. No se ha confirmado la señal 3D. Inténtalo de nuevo cuando vuelva la imagen 2D. Sin imagen: las gafas están en un modo que este teléfono no puede enviar. Mantén pulsado su botón 2D/3D o desconéctalas y vuelve a conectarlas. Frecuencia en 3D diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 4ad9c648e..16a40ba48 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -2213,7 +2213,6 @@ Virkistystaajuus 3D (rinnakkain) Kumpikin silmä saa oman puoliskonsa 3840×1080-kuvasta. Jos kuva katoaa, pidä lasien 2D/3D-painiketta pohjassa. - Tämä puhelin ei pysty lähettämään 3D:n vaatimaa 3840×1080-signaalia. VR-pelit näyttävät yhden silmän näkymän pään seurannalla. 3D-signaalia ei voitu vahvistaa. Yritä uudelleen, kun 2D-kuva palautuu. Ei kuvaa: lasit ovat tilassa, jota tämä puhelin ei pysty lähettämään. Pidä niiden 2D/3D-painiketta pohjassa tai irrota ja kytke ne uudelleen. 3D-virkistystaajuus diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0732f03ba..de7505876 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -2246,7 +2246,6 @@ Fréquence d\'affichage 3D (côte à côte) Chaque œil reçoit sa propre moitié d\'une image de 3840×1080. Si l\'image disparaît, maintenez le bouton 2D/3D des lunettes. - Ce téléphone ne peut pas envoyer le signal 3840×1080 nécessaire à la 3D. Les jeux VR affichent la vue d\'un seul œil, avec le suivi de la tête. Le signal 3D n\'a pas été confirmé. Réessayez une fois l\'image 2D rétablie. Pas d\'image : les lunettes sont dans un mode que ce téléphone ne peut pas envoyer. Maintenez leur bouton 2D/3D, ou débranchez-les puis rebranchez-les. Fréquence en 3D diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 84e0b9440..63ff2b876 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -2213,7 +2213,6 @@ रिफ्रेश रेट 3D (साइड-बाय-साइड) हर आंख को 3840×1080 तस्वीर का अपना आधा हिस्सा मिलता है। अगर तस्वीर चली जाए, तो चश्मे का 2D/3D बटन दबाकर रखें। - यह फ़ोन 3D के लिए ज़रूरी 3840×1080 सिग्नल नहीं भेज सकता। VR गेम हेड ट्रैकिंग के साथ एक आंख का व्यू दिखाते हैं। 3D सिग्नल की पुष्टि नहीं हुई। 2D तस्वीर वापस आने पर फिर कोशिश करें। कोई तस्वीर नहीं: चश्मा ऐसे मोड में है जिसे यह फ़ोन नहीं भेज सकता। उसका 2D/3D बटन दबाकर रखें, या केबल निकालकर फिर से लगाएं। 3D रिफ्रेश रेट diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 25a0af979..2254d45a4 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -2213,7 +2213,6 @@ Frissítési frekvencia 3D (egymás melletti) Mindkét szem egy 3840×1080-as kép saját felét kapja. Ha a kép eltűnik, tartsa lenyomva a szemüveg 2D/3D gombját. - Ez a telefon nem tudja küldeni a 3D-hez szükséges 3840×1080-as jelet. A VR-játékok egy szem nézetét mutatják, fejkövetéssel. Nem sikerült megerősíteni a 3D-jelet. Próbáld újra, ha visszatér a 2D-kép. Nincs kép: a szemüveg olyan módban van, amelyet ez a telefon nem tud küldeni. Tartsa lenyomva a 2D/3D gombját, vagy húzza ki, és dugja vissza. 3D frissítési frekvencia diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index e1f2afdea..92ac175c1 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -2188,7 +2188,6 @@ Refresh rate 3D (berdampingan) Setiap mata mendapat separuhnya sendiri dari gambar 3840×1080. Jika gambar hilang, tahan tombol 2D/3D kacamata. - Ponsel ini tidak bisa mengirim sinyal 3840×1080 yang dibutuhkan 3D. Game VR menampilkan pandangan satu mata, dengan pelacakan kepala. Sinyal 3D belum terkonfirmasi. Coba lagi setelah gambar 2D kembali. Tidak ada gambar: kacamata dalam mode yang tidak bisa dikirim ponsel ini. Tahan tombol 2D/3D-nya, atau cabut lalu colokkan lagi. Refresh rate 3D diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index f87a072bf..151e73c58 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -2238,7 +2238,6 @@ Frequenza di aggiornamento 3D (affiancato) Ogni occhio riceve la propria metà di un\'immagine 3840×1080. Se l\'immagine scompare, tieni premuto il pulsante 2D/3D degli occhiali. - Questo telefono non può inviare il segnale 3840×1080 richiesto dal 3D. I giochi VR mostrano la vista di un solo occhio, con il tracciamento della testa. Il segnale 3D non è stato confermato. Riprova quando torna l\'immagine 2D. Nessuna immagine: gli occhiali sono in una modalità che questo telefono non può inviare. Tieni premuto il loro pulsante 2D/3D, oppure scollegali e ricollegali. Frequenza di aggiornamento 3D diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 1c881e40b..f473b5f5c 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -2196,7 +2196,6 @@ リフレッシュレート 3D(サイドバイサイド) 左右の目に 3840×1080 の画像の半分ずつを表示します。映像が消えたら、グラスの 2D/3D ボタンを長押ししてください。 - このスマートフォンは 3D に必要な 3840×1080 の信号を出力できません。VR ゲームはヘッドトラッキング付きで片目の映像を表示します。 3D 信号を確認できませんでした。2D 映像が戻ってから再試行してください。 映像なし: グラスがこのスマートフォンで出力できないモードになっています。2D/3D ボタンを長押しするか、抜き差ししてください。 3D のリフレッシュレート diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 6f8adfa96..511cda3d8 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -2196,7 +2196,6 @@ 주사율 3D (사이드 바이 사이드) 각 눈이 3840×1080 화면의 절반씩을 봅니다. 화면이 끊기면 안경의 2D/3D 버튼을 길게 누르세요. - 이 휴대폰은 3D에 필요한 3840×1080 신호를 보낼 수 없습니다. VR 게임은 헤드 트래킹과 함께 한쪽 눈의 화면을 보여 줍니다. 3D 신호를 확인하지 못했습니다. 2D 화면이 돌아오면 다시 시도하세요. 화면 없음: 안경이 이 휴대폰에서 보낼 수 없는 모드에 있습니다. 2D/3D 버튼을 길게 누르거나 안경을 뺐다가 다시 연결하세요. 3D 주사율 diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index 91cc0aef1..173795731 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -2188,7 +2188,6 @@ Kadar penyegaran 3D (sebelah-menyebelah) Setiap mata mendapat separuh gambar 3840×1080 sendiri. Jika gambar hilang, tahan butang 2D/3D cermin mata. - Telefon ini tidak dapat menghantar isyarat 3840×1080 yang diperlukan oleh 3D. Permainan VR memaparkan pandangan sebelah mata, dengan penjejakan kepala. Isyarat 3D tidak dapat disahkan. Cuba lagi selepas gambar 2D kembali. Tiada gambar: cermin mata dalam mod yang tidak dapat dihantar oleh telefon ini. Tahan butang 2D/3D-nya, atau cabut dan pasang semula. Kadar penyegaran 3D diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 4709f2412..166541b7d 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -2213,7 +2213,6 @@ Oppdateringsfrekvens 3D (side ved side) Hvert øye får sin egen halvdel av et bilde på 3840×1080. Hvis bildet faller ut, holder du inne 2D/3D-knappen på brillene. - Denne telefonen kan ikke sende 3840×1080-signalet som 3D krever. VR-spill viser bildet for ett øye, med hodesporing. 3D-signalet ble ikke bekreftet. Prøv igjen når 2D-bildet er tilbake. Ikke noe bilde: brillene er i en modus denne telefonen ikke kan sende. Hold inne 2D/3D-knappen deres, eller koble dem fra og til igjen. 3D-oppdateringsfrekvens diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 98999434b..da072fccb 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -2213,7 +2213,6 @@ Verversingssnelheid 3D (side-by-side) Elk oog krijgt een eigen helft van een beeld van 3840×1080. Valt het beeld weg, houd dan de 2D/3D-knop van de bril ingedrukt. - Deze telefoon kan het 3840×1080-signaal voor 3D niet versturen. VR-games tonen het beeld van één oog, met hoofdtracking. Het 3D-signaal is niet bevestigd. Probeer het opnieuw zodra het 2D-beeld terug is. Geen beeld: de bril staat in een modus die deze telefoon niet kan versturen. Houd de 2D/3D-knop ingedrukt, of koppel de bril los en sluit hem opnieuw aan. 3D-verversingssnelheid diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 2b6a59613..34426e102 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -2263,7 +2263,6 @@ Częstotliwość odświeżania 3D (obok siebie) Każde oko dostaje własną połowę obrazu 3840×1080. Jeśli obraz zniknie, przytrzymaj przycisk 2D/3D na okularach. - Ten telefon nie może wysłać sygnału 3840×1080 potrzebnego do 3D. Gry VR pokazują widok jednego oka ze śledzeniem głowy. Nie udało się potwierdzić sygnału 3D. Spróbuj ponownie, gdy wróci obraz 2D. Brak obrazu: okulary są w trybie, którego ten telefon nie obsługuje. Przytrzymaj ich przycisk 2D/3D albo odłącz je i podłącz ponownie. Częstotliwość odświeżania w 3D diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 7daeca20f..823293c37 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -2238,7 +2238,6 @@ Taxa de atualização 3D (lado a lado) Cada olho recebe a sua metade de uma imagem de 3840×1080. Se a imagem desaparecer, mantenha premido o botão 2D/3D dos óculos. - Este telemóvel não consegue enviar o sinal de 3840×1080 de que o 3D precisa. Os jogos de RV mostram a vista de um só olho, com seguimento da cabeça. O sinal 3D não foi confirmado. Tenta novamente quando a imagem 2D voltar. Sem imagem: os óculos estão num modo que este telemóvel não consegue enviar. Mantenha premido o botão 2D/3D ou desligue-os e volte a ligá-los. Taxa de atualização em 3D diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 6639744ec..55a3204fb 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -2238,7 +2238,6 @@ Taxa de atualização 3D (lado a lado) Cada olho recebe a própria metade de uma imagem de 3840×1080. Se a imagem sumir, mantenha pressionado o botão 2D/3D dos óculos. - Este celular não consegue enviar o sinal de 3840×1080 de que o 3D precisa. Jogos de RV mostram a visão de um olho só, com rastreamento da cabeça. O sinal 3D não foi confirmado. Tente novamente quando a imagem 2D voltar. Sem imagem: os óculos estão em um modo que este celular não consegue enviar. Mantenha pressionado o botão 2D/3D deles ou desconecte e reconecte os óculos. Taxa de atualização em 3D diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index f07b1862b..112e1c378 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -2238,7 +2238,6 @@ Rată de reîmprospătare 3D (alăturat) Fiecare ochi primește propria jumătate dintr-o imagine de 3840×1080. Dacă imaginea dispare, ține apăsat butonul 2D/3D al ochelarilor. - Acest telefon nu poate trimite semnalul de 3840×1080 de care are nevoie 3D-ul. Jocurile VR arată imaginea unui singur ochi, cu urmărirea capului. Semnalul 3D nu a fost confirmat. Încearcă din nou când revine imaginea 2D. Nicio imagine: ochelarii sunt într-un mod pe care acest telefon nu îl poate trimite. Ține apăsat butonul lor 2D/3D sau deconectează-i și reconectează-i. Rată de reîmprospătare 3D diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index a9d7556b8..bbb8e9741 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -2271,7 +2271,6 @@ Частота обновления 3D (стереопара) Каждый глаз получает свою половину изображения 3840×1080. Если изображение пропадёт, удерживайте кнопку 2D/3D на очках. - Этот телефон не может передать сигнал 3840×1080, нужный для 3D. VR-игры показывают вид одного глаза с отслеживанием головы. Сигнал 3D не подтверждён. Повторите попытку, когда вернётся изображение 2D. Нет изображения: очки в режиме, который этот телефон не может передать. Удерживайте их кнопку 2D/3D или отключите и снова подключите очки. Частота в 3D diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 65f556d72..7ff12a91d 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -2213,7 +2213,6 @@ Uppdateringsfrekvens 3D (sida vid sida) Varje öga får sin egen halva av en bild på 3840×1080. Om bilden försvinner håller du in glasögonens 2D/3D-knapp. - Den här telefonen kan inte skicka den 3840×1080-signal som 3D kräver. VR-spel visar ett ögas vy, med huvudspårning. 3D-signalen kunde inte bekräftas. Försök igen när 2D-bilden är tillbaka. Ingen bild: glasögonen är i ett läge som den här telefonen inte kan skicka. Håll in deras 2D/3D-knapp, eller koppla ur dem och anslut dem igen. 3D-uppdateringsfrekvens diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index da2007233..43f64f8dc 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -2188,7 +2188,6 @@ อัตรารีเฟรช 3D (แบบเคียงข้าง) ตาแต่ละข้างจะเห็นภาพคนละครึ่งของภาพขนาด 3840×1080 หากภาพหายไป ให้กดปุ่ม 2D/3D ของแว่นตาค้างไว้ - โทรศัพท์นี้ส่งสัญญาณ 3840×1080 ที่ 3D ต้องใช้ไม่ได้ เกม VR จะแสดงภาพของตาข้างเดียวพร้อมการติดตามศีรษะ ยังยืนยันสัญญาณ 3D ไม่ได้ ลองอีกครั้งเมื่อภาพ 2D กลับมา ไม่มีภาพ: แว่นตาอยู่ในโหมดที่โทรศัพท์นี้ส่งสัญญาณให้ไม่ได้ กดปุ่ม 2D/3D ของแว่นตาค้างไว้ หรือถอดสายแล้วเสียบใหม่ อัตรารีเฟรช 3D diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 8018af6e4..3a36cd288 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -2213,7 +2213,6 @@ Yenileme hızı 3D (yan yana) Her göz 3840×1080 görüntünün kendi yarısını alır. Görüntü kesilirse gözlüğün 2D/3D düğmesini basılı tutun. - Bu telefon 3D\'nin gerektirdiği 3840×1080 sinyalini gönderemez. VR oyunları, kafa takibiyle tek gözün görüntüsünü gösterir. 3D sinyali doğrulanamadı. 2D görüntü geri geldiğinde tekrar deneyin. Görüntü yok: gözlük bu telefonun gönderemediği bir modda. Gözlüğün 2D/3D düğmesini basılı tutun ya da kablosunu çıkarıp yeniden takın. 3D yenileme hızı diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 1835002dd..a3335e76d 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -2263,7 +2263,6 @@ Частота оновлення 3D (стереопара) Кожне око отримує свою половину зображення 3840×1080. Якщо зображення зникне, утримуйте кнопку 2D/3D на окулярах. - Цей телефон не може передати сигнал 3840×1080, потрібний для 3D. VR-ігри показують вигляд для одного ока з відстеженням голови. Сигнал 3D не підтверджено. Спробуйте знову, коли повернеться зображення 2D. Немає зображення: окуляри в режимі, який цей телефон не може передати. Утримуйте їхню кнопку 2D/3D або від’єднайте й знову під’єднайте їх. Частота оновлення в 3D diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 8253b3ac0..8cc7bb7c9 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -2188,7 +2188,6 @@ Tốc độ làm mới 3D (cạnh nhau) Mỗi mắt nhận một nửa riêng của hình 3840×1080. Nếu mất hình, hãy giữ nút 2D/3D của kính. - Điện thoại này không thể xuất tín hiệu 3840×1080 mà 3D cần. Game VR hiển thị góc nhìn của một mắt, có theo dõi đầu. Chưa xác nhận được tín hiệu 3D. Hãy thử lại khi hình ảnh 2D trở lại. Không có hình: kính đang ở chế độ mà điện thoại này không thể xuất. Hãy giữ nút 2D/3D của kính, hoặc rút ra rồi cắm lại. Tốc độ làm mới 3D diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index baa162a19..936be0262 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -2196,7 +2196,6 @@ 刷新率 3D(左右分屏) 每只眼睛各看 3840×1080 画面的一半。如果画面消失,请长按眼镜上的 2D/3D 按钮。 - 这部手机无法输出 3D 所需的 3840×1080 信号。VR 游戏会显示单眼画面,并带有头部追踪。 未能确认 3D 信号。请在 2D 画面恢复后重试。 无画面:眼镜处于这部手机无法输出的模式。请长按眼镜的 2D/3D 按钮,或拔下后重新连接。 3D 刷新率 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 9a54f5e83..88ff38a40 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -2196,7 +2196,6 @@ 更新率 3D(左右並排) 每隻眼睛各看 3840×1080 畫面的一半。如果畫面消失,請長按眼鏡上的 2D/3D 按鈕。 - 這部手機無法輸出 3D 所需的 3840×1080 訊號。VR 遊戲會顯示單眼畫面,並配合頭部追蹤。 未能確認 3D 訊號。請在 2D 畫面恢復後重試。 沒有畫面:眼鏡處於這部手機無法輸出的模式。請長按眼鏡的 2D/3D 按鈕,或拔除後重新連接。 3D 更新率 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 87110ff11..a89174f0b 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -2196,7 +2196,6 @@ 更新率 3D(左右並排) 每隻眼睛各看 3840×1080 畫面的一半。如果畫面消失,請長按眼鏡上的 2D/3D 按鈕。 - 這支手機無法輸出 3D 所需的 3840×1080 訊號。VR 遊戲會顯示單眼畫面,並搭配頭部追蹤。 未能確認 3D 訊號。請在 2D 畫面恢復後再試。 沒有畫面:眼鏡處於這支手機無法輸出的模式。請長按眼鏡的 2D/3D 按鈕,或拔除後重新連接。 3D 更新率 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7075de27e..bb5e15072 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2279,7 +2279,6 @@ Refresh rate 3D (side-by-side) Each eye gets its own half of a 3840×1080 picture. If the picture drops out, hold the glasses\' 2D/3D button. - This phone can\'t send the 3840×1080 signal 3D needs. VR games show one eye\'s view, with head tracking. The 3D signal was not confirmed. Try again once the 2D picture returns. No picture: the glasses are in a mode this phone can\'t send. Hold their 2D/3D button, or unplug and replug them. 3D refresh rate diff --git a/app/src/test/java/com/droiddeck/launcher/glasses/GlassesUsbTest.kt b/app/src/test/java/com/droiddeck/launcher/glasses/GlassesUsbTest.kt index 451dc7fb7..830f8dfe8 100644 --- a/app/src/test/java/com/droiddeck/launcher/glasses/GlassesUsbTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/glasses/GlassesUsbTest.kt @@ -7,6 +7,7 @@ import android.hardware.usb.UsbDevice import android.hardware.usb.UsbManager import android.os.Handler import android.os.Looper +import android.os.SystemClock import android.view.Display import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Assert.assertEquals @@ -24,9 +25,11 @@ import org.robolectric.annotation.Implements import org.robolectric.annotation.LooperMode import org.robolectric.shadow.api.Shadow import org.robolectric.shadows.ShadowDisplayManager +import org.robolectric.shadows.ShadowSystemClock import org.robolectric.shadows.ShadowUsbManager import org.robolectric.util.ReflectionHelpers import org.robolectric.util.ReflectionHelpers.ClassParameter.from +import java.time.Duration @RunWith(RobolectricTestRunner::class) @Config(sdk = [28], manifest = Config.NONE, shadows = [GlassesUsbTest.RecordingUsbManager::class]) @@ -75,7 +78,7 @@ class GlassesUsbTest { refresh(ask = true) assertTrue(manager.requests.isEmpty()) assertEquals(Glasses.Permission.NONE, state.value.permission) - assertTrue(state.value.present) + assertEquals(pid == 0x1201, state.value.present) assertFalse(state.value.controllable) assertFalse(state.value.connected) } @@ -97,6 +100,36 @@ class GlassesUsbTest { assertEquals(Glasses.Permission.NONE, state.value.permission) } + @Test fun noSignalStereoFailureIsRememberedPerModel() { + val prefs = RuntimeEnvironment.getApplication().getSharedPreferences("glasses", Context.MODE_PRIVATE) + prefs.edit().clear().commit() + val hdmi = ReflectionHelpers.getStaticField(Display::class.java, "TYPE_HDMI") + val displayId = ShadowDisplayManager.addDisplay("w640dp-h480dp", hdmi) + val displays = RuntimeEnvironment.getApplication().getSystemService(DisplayManager::class.java) + shadowOf(displays.getDisplay(displayId)).setFlags(Display.FLAG_PRESENTATION) + val pro = device(0x1019) + manager.devices[pro.deviceName] = pro + refresh() + assertTrue(state.value.noSignal) + (field("modeState").get(null) as GlassesModeState).sent(true, SystemClock.uptimeMillis()) + ShadowSystemClock.advanceBy(Duration.ofSeconds(21)) + refresh() + assertEquals(GlassesState.SBS_FAILS, state.value.sbsSupport) + assertEquals(setOf("1019"), prefs.getStringSet("sbsFailed", null)) + ShadowDisplayManager.removeDisplay(displayId) + + val xr = device(0x101d) + manager.devices.clear() + manager.devices[xr.deviceName] = xr + refresh() + assertEquals(GlassesState.SBS_UNKNOWN, state.value.sbsSupport) + manager.devices.clear() + manager.devices[pro.deviceName] = pro + refresh() + assertEquals(GlassesState.SBS_FAILS, state.value.sbsSupport) + assertFalse(state.value.can3d) + } + private fun refresh(ask: Boolean = false) { Glasses::class.java.getDeclaredMethod("refresh", Boolean::class.javaPrimitiveType) .apply { isAccessible = true }.invoke(Glasses, ask) diff --git a/app/src/test/java/com/droiddeck/launcher/glasses/VitureProtocolTest.kt b/app/src/test/java/com/droiddeck/launcher/glasses/VitureProtocolTest.kt index f0696caf7..591a2e9b1 100644 --- a/app/src/test/java/com/droiddeck/launcher/glasses/VitureProtocolTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/glasses/VitureProtocolTest.kt @@ -118,14 +118,6 @@ class VitureProtocolTest { assertEquals("VITURE One", VitureProtocol.model(0x1011, "Something else")!!.name) } - @Test fun modeHelpers() { - listOf(0x32, 0x35, 0x42, 0x45).forEach { assertTrue(VitureProtocol.modeIsSbs(it)) } - listOf(0x31, 0x33, 0x34, 0x41, 0x44).forEach { assertFalse(VitureProtocol.modeIsSbs(it)) } - assertEquals(120, VitureProtocol.modeHz(0x34)) - assertEquals(90, VitureProtocol.modeHz(0x35)) - assertEquals(60, VitureProtocol.modeHz(0x32)) - } - @Test fun imuPrefersTheQuaternion() { val half = Math.toRadians(30.0) / 2 val payload = ByteBuffer.allocate(40) diff --git a/docs/agent-control.md b/docs/agent-control.md index 3539708c0..0188b2505 100644 --- a/docs/agent-control.md +++ b/docs/agent-control.md @@ -132,6 +132,7 @@ tools/droiddeckctl glasses raw 0x0007 Glasses status reports `deviceMode` from the MCU separately from `sbs`, which requires a physical stereo display mode. `switching3d` marks the bounded confirmation window. A successful mode-command reply alone does not confirm 3D. +In `frames`, a report whose checksum does not match ends in `crc!`. A sleeping device suspends the session and hides the app's windows from `ui dump`; `wake` first. diff --git a/docs/cardboard-vr.md b/docs/cardboard-vr.md index 4b35d7cb6..ae25df571 100644 --- a/docs/cardboard-vr.md +++ b/docs/cardboard-vr.md @@ -139,13 +139,16 @@ Verified on the AYN Thor (SD 8 Gen 2, Turnip): SteamVR 2.17.10 starts with the D the compositor runs driver-direct at about 60 fps, the dashboard (Steam's library in VR, with the controller's laser) shows cleanly in both eyes at the panel's resolution while Steam keeps running, and a pose written to the shared file turns SteamVR's view. Started from the drawer button through the agent. -With VITURE glasses connected, the Steam page also offers **Play SteamVR**, which starts Steam and then +With VITURE glasses connected and under DroidDeck's control, the Steam page also offers **Play SteamVR**, which starts Steam and then SteamVR (installing it first if needed), and stops SteamVR when the glasses are unplugged. **Play Steam** never starts it: while SteamVR runs, Steam keeps its own window in front of flat games. +Through a VITURE Mobile Dock the glasses are only a display: the dock does not pass their controls or +head tracking to the phone, so there is no Play SteamVR, refresh, 3D or tracking setting. Stereo also requires a physical side-by-side display mode. On the tested Astra 2 firmware the Pro XR command succeeds but DisplayPort falls back to 640×480; -direct-connection stereo remains unresolved. See the +direct-connection stereo remains unresolved. DroidDeck remembers such a fallback for that glasses +model and stops asking for 3D on its own; **Retry** in the glasses settings tries again. See the [Astra/VITURE investigation](development/astra-viture-stereo.md) for measured results, app recovery behavior, and the unverified powered-adapter design. diff --git a/docs/development/astra-viture-stereo.md b/docs/development/astra-viture-stereo.md index 9d0a34683..b1debfa0f 100644 --- a/docs/development/astra-viture-stereo.md +++ b/docs/development/astra-viture-stereo.md @@ -60,9 +60,17 @@ checksum. The Astra negotiated 1920×1080/120, but enumerated only the dock's USB interfaces, not the glasses' `35ca:101d` control/IMU interfaces. A one-second DroidDeck IMU sample returned zero packets. -DroidDeck now keeps the dock's display available without requesting unsupported -USB control access. The updated build was installed on this connection: no USB -permission prompt appeared, video remained at 1080p/120 and tracking stayed off. +DroidDeck treats the glasses as a display behind the dock: no USB permission +prompt, video at 1080p/120 on the glasses, no Play SteamVR, refresh, 3D or tracking +settings. The dock alone, without glasses, does not count as glasses. + +VITURE's updater calls this dock P9 (app `35ca:3011`, bootloader `35ca:3010`). Its +HID messages cover dock versions, battery, inputs and LEDs; none sets a display +mode or forwards glasses commands or IMU data. `2.01.03.0.03` is public release +`1.03.0.03_20250709`; the latest, `1.03.0.06_20260409`, adds Beast support and +updater fixes only. The updater also reads a Lontium LT8668 in the dock, a +DisplayPort receiver with its own EDID handling, so the tablet most likely sees the +dock as its DisplayPort sink rather than the glasses. [VITURE's Pro Dock specifications](https://www.viture.com/product/viture-pro-mobile-dock) confirm 3840×1080/60 video support and exclude phone features requiring @@ -107,9 +115,11 @@ A wide screenshot or `wm size` override alone does not meet those conditions. ## App safeguards -DroidDeck waits up to 20 seconds for physical stereo confirmation. A temporary -low-resolution mode does not permanently blacklist the device. Failed attempts -offer manual retry once the 2D signal returns; automatic retries do not loop. +DroidDeck waits up to 20 seconds for physical stereo confirmation. When the +attempt ends at a low-resolution fallback, DroidDeck remembers it for that glasses +model on this device, so Play SteamVR stops requesting 3D after a restart; **Retry** in the +glasses settings clears it. A display that stays missing, such as an unanswered +cast prompt, fails only that attempt. Automatic retries do not loop. USB mode reports are tracked separately from the physical display. When a session enabled 3D, stopping it requests a return to 2D, including during negotiation; this firmware can still require a cable reconnect. Output timing follows @@ -118,28 +128,3 @@ the active glasses display rather than the tablet's requested refresh rate. Keep hardware experiments separate from game loads. End the session, force-stop DroidDeck, and reopen its home screen after each game test. Read current HAL temperatures rather than cached thermal-service samples. - -## Validation of this update - -- Full debug APK build, including the Rust store engine; native-library, session - asset and runtime-input packaging checks passed. -- 846 Android unit tests across 141 suites passed, including mode deadlines, - reconnect ownership, stale USB callbacks, dock selection and permission handling, - truncated USB frames and model-specific protocol cases. -- Four host tests passed, including native refresh notification, event coalescing, - full-pipe handling and Cardboard output checks. -- On the Astra: brightness changed and was restored; IMU delivered 400 packets - in two seconds; Steam reached its first frame in 4.9 seconds and exited cleanly. - Wayland reported the actual 120 Hz tablet mode. Four complete steady-state - ten-second windows recorded 116.8–119.5 screen frames/s in Steam's UI. -- After reconnect: a fresh stereo attempt remained pending during the 640×480 - fallback, failed at the 20-second deadline and issued one 2D request. The MCU - refused that request; another 28 seconds of observation showed no retry loop. -- DroidDeck was force-stopped and relaunched after testing. Debugging tools and - temporary logging/display overrides were restored. Current HAL temperatures - after the session were 25°C battery and 28.3°C skin. -- The dock checks ran without a game load; the idle battery temperature was 24°C. - -These checks do not establish stereo-game performance or a sustained thermal -baseline. Direct-cable stereo and the powered-adapter design remain unverified -as working solutions on this Astra firmware. diff --git a/tools/vr/src/driver_droiddeck.cpp b/tools/vr/src/driver_droiddeck.cpp index 2ea58195b..9a84ad896 100644 --- a/tools/vr/src/driver_droiddeck.cpp +++ b/tools/vr/src/driver_droiddeck.cpp @@ -102,16 +102,26 @@ public: void WaitForPresent() override {} bool GetTimeSinceLastVsync(float *seconds, uint64_t *counter) override { using namespace std::chrono; - double t = duration(steady_clock::now().time_since_epoch()).count(); - double period = VsyncPeriodSeconds(); - *seconds = (float)fmod(t, period); - *counter = (uint64_t)(t / period); + const double period = VsyncPeriodSeconds(); + std::lock_guard lock(vsyncMutex_); + const double t = duration(steady_clock::now().time_since_epoch()).count(); + if (period != period_) { + vsyncBase_ = period_ > 0.0 ? vsyncBase_ + (uint64_t)((t - vsyncAt_) / period_) : (uint64_t)(t / period); + vsyncAt_ = t; + period_ = period; + } + const double since = t - vsyncAt_; + *seconds = (float)fmod(since, period); + *counter = vsyncBase_ + (uint64_t)(since / period); return true; } private: uint64_t frames_ = 0; std::chrono::steady_clock::time_point last_; + std::mutex vsyncMutex_; + double period_ = 0.0, vsyncAt_ = 0.0; + uint64_t vsyncBase_ = 0; }; class Vsync { @@ -490,6 +500,8 @@ public: if (pose_.Read(&hp) && hp.tan_half_fov > 0.0f) { tanHalf_ = hp.tan_half_fov; if (hp.eye_aspect > 0.0f) eyeAspect_ = hp.eye_aspect; + sentMono_ = (hp.flags & DROIDDECK_HEADPOSE_MONO) != 0; + renderScale_ = sentMono_ ? 1.0f : 1.4f; } if (pose_.Read(&hp) && hp.display_hz >= 24.0f) hz = hp.display_hz; Vsync::SetRate(hz); @@ -599,7 +611,8 @@ private: void UpdateEye(const droiddeck_headpose &hp) { sentTan_ = tanHalf_ = hp.tan_half_fov; sentAspect_ = eyeAspect_ = hp.eye_aspect; - renderScale_ = (hp.flags & DROIDDECK_HEADPOSE_MONO) ? 1.0f : 1.4f; + sentMono_ = (hp.flags & DROIDDECK_HEADPOSE_MONO) != 0; + renderScale_ = sentMono_ ? 1.0f : 1.4f; HmdRect2_t r; r.vTopLeft.v[0] = -tanHalf_ * eyeAspect_; r.vTopLeft.v[1] = -tanHalf_; @@ -635,7 +648,8 @@ private: if (fresh) { lastSeq = hp.seq; if (hp.eye_w > 0 && hp.tan_half_fov > 0.0f && hp.eye_aspect > 0.0f && - (fabsf(hp.tan_half_fov - sentTan_) > 1e-3f || fabsf(hp.eye_aspect - sentAspect_) > 1e-3f)) + (fabsf(hp.tan_half_fov - sentTan_) > 1e-3f || fabsf(hp.eye_aspect - sentAspect_) > 1e-3f || + ((hp.flags & DROIDDECK_HEADPOSE_MONO) != 0) != sentMono_)) UpdateEye(hp); if (hp.display_hz >= 24.0f && fabsf(hp.display_hz - sentHz_) > 0.5f) UpdateRate(hp.display_hz); const bool worn = (hp.flags & DROIDDECK_HEADPOSE_ENABLED) != 0; @@ -690,6 +704,7 @@ private: int32_t width_ = 1920, height_ = 1080; std::atomic tanHalf_{1.0f}, eyeAspect_{0.889f}, renderScale_{1.4f}; float sentTan_ = 0.0f, sentAspect_ = 0.0f, sentHz_ = 0.0f; + bool sentMono_ = false; PropertyContainerHandle_t container_ = k_ulInvalidPropertyContainer; public: void SetHands(CardboardController *left, CardboardController *right) { hands_[0] = left; hands_[1] = right; } diff --git a/tools/vr/src/presenter.cpp b/tools/vr/src/presenter.cpp index 45c91392d..535203707 100644 --- a/tools/vr/src/presenter.cpp +++ b/tools/vr/src/presenter.cpp @@ -15,7 +15,6 @@ typedef unsigned long VisualID; #include #include #include -#include #include #include #include @@ -460,7 +459,7 @@ private: VkSemaphoreCreateInfo semci = {VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, nullptr, 0}; vk_.CreateSemaphore(dev_, &semci, nullptr, &acquired_); vk_.CreateSemaphore(dev_, &semci, nullptr, &rendered_); - if (!CreateSwapchain()) return false; + if (!CreateSwapchain() && !wayland_) return false; Log("presenter: Vulkan on %s, queue family %u", props.deviceName, qfam_); return true; } diff --git a/tools/vr/src/vk_layer_droiddeck_vr.c b/tools/vr/src/vk_layer_droiddeck_vr.c index e88772fab..7d3ff7026 100644 --- a/tools/vr/src/vk_layer_droiddeck_vr.c +++ b/tools/vr/src/vk_layer_droiddeck_vr.c @@ -319,6 +319,23 @@ static uint32_t mode_mhz(void) { static uint64_t frame_ns(void) { return 1000000000000ull / mode_mhz(); } +static pthread_mutex_t g_vblank_lock = PTHREAD_MUTEX_INITIALIZER; +static uint64_t g_vblank_base, g_vblank_at, g_vblank_frame; + +static uint64_t vblank_count(void) { + uint64_t f = frame_ns(); + pthread_mutex_lock(&g_vblank_lock); + uint64_t t = now_ns(); + if (f != g_vblank_frame) { + g_vblank_base = g_vblank_frame ? g_vblank_base + (t - g_vblank_at) / g_vblank_frame : t / f; + g_vblank_at = t; + g_vblank_frame = f; + } + uint64_t n = g_vblank_base + (t - g_vblank_at) / f; + pthread_mutex_unlock(&g_vblank_lock); + return n; +} + static VkDisplayPropertiesKHR display_props(void) { mode_init(); VkDisplayPropertiesKHR p = {0}; @@ -454,7 +471,7 @@ static VkResult VKAPI_CALL RegisterDisplayEventEXT(VkDevice d, VkDisplayKHR disp } static VkResult VKAPI_CALL GetSwapchainCounterEXT(VkDevice d, VkSwapchainKHR sc, VkSurfaceCounterFlagBitsEXT counter, uint64_t *value) { mode_init(); - *value = now_ns() / frame_ns(); + *value = vblank_count(); return VK_SUCCESS; }