Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
c66a30c
Add external display output (USB-C/HDMI/Miracast/XR glasses)
maxjivi05 Jun 2, 2026
344d79c
Add Viture glasses USB control, refresh-rate force, and Output-tab gl…
maxjivi05 Jun 2, 2026
bd94ec5
Switch external monitor mode in hardware when supported; honest rende…
maxjivi05 Jun 2, 2026
415748d
Merge remote-tracking branch 'origin/main' into display-output
maxjivi05 Jun 2, 2026
66cd4bd
Viture glasses: confirmed MCU control channel + volume control
maxjivi05 Jun 4, 2026
f609ee9
Merge remote-tracking branch 'origin/main' into display-output
maxjivi05 Jun 4, 2026
01a15d2
Viture: fix electrochromic film opcode (binary 0x000E, not stepped 0x…
maxjivi05 Jun 4, 2026
cdab91a
Glasses: pre-container control panel in the library + automatic displ…
maxjivi05 Jun 4, 2026
90fd2ee
Glasses panel: two-column layout, 100% defaults, sent on connect
maxjivi05 Jun 4, 2026
ef7ad48
Glasses panel: group toggles under Refresh (left), sliders together (…
maxjivi05 Jun 4, 2026
1150764
Glasses: default Sunblock film on (matches the glasses' power-on state)
maxjivi05 Jun 4, 2026
9040957
Glasses: stop the in-game drawer/display-mode storm on slider drag
maxjivi05 Jun 4, 2026
126c07b
Glasses: use the Material Symbols 'Eyeglasses 2' icon for the library…
maxjivi05 Jun 4, 2026
d18733f
Performance HUD: phone gauge HUD + trackpad when a controller + exter…
maxjivi05 Jun 4, 2026
6955300
Performance HUD: half-screen gauge panel, shown whenever controller +…
maxjivi05 Jun 4, 2026
40d0b6b
Performance HUD: render in the Compose host (fix nested-ComposeView c…
maxjivi05 Jun 4, 2026
c4dfc57
Performance HUD: pull FPS/renderer from the external-display game window
maxjivi05 Jun 5, 2026
583bda7
Merge branch 'WinNative-Emu:main' into external-monitor
maxjivi05 Jun 5, 2026
5b1cb7a
Merge origin/main into external-monitor
maxjivi05 Jun 7, 2026
953e710
Merge branch 'WinNative-Emu:main' into external-monitor
maxjivi05 Jun 8, 2026
eca3d3c
Merge branch 'WinNative-Emu:main' into external-monitor
maxjivi05 Jun 8, 2026
bdc9362
Merge branch 'WinNative-Emu:main' into external-monitor
maxjivi05 Jun 9, 2026
9c91e7d
Add in-session game recording
maxjivi05 Jun 9, 2026
d75d2fd
Fix external-display switch check and Viture 3D-off refresh
maxjivi05 Jun 9, 2026
184b08d
Merge branch 'main' into external-monitor
maxjivi05 Jun 11, 2026
439e1fe
Merge branch 'WinNative-Emu:main' into external-monitor
maxjivi05 Jun 11, 2026
c0f51ad
Merge remote-tracking branch 'origin/main' into external-monitor
maxjivi05 Jun 11, 2026
8172364
Merge branch 'WinNative-Emu:main' into external-monitor
maxjivi05 Jun 15, 2026
522b1a9
Merge remote-tracking branch 'origin/main' into external-monitor
maxjivi05 Jun 16, 2026
a4d06b0
Merge branch 'WinNative-Emu:main' into external-monitor
maxjivi05 Jun 17, 2026
ea35463
Merge remote-tracking branch 'upstream/main' into external-monitor
maxjivi05 Jun 18, 2026
992d30c
Merge branch 'WinNative-Emu:main' into external-monitor
maxjivi05 Jun 19, 2026
56b5b2d
Merge branch 'main' into external-monitor
maxjivi05 Jun 19, 2026
ef4a355
Merge remote-tracking branch 'upstream/main' into external-monitor
maxjivi05 Jun 19, 2026
eae3a04
Merge branch 'WinNative-Emu:main' into external-monitor
maxjivi05 Jun 20, 2026
de46e50
Merge branch 'WinNative-Emu:main' into external-monitor
maxjivi05 Jun 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions app/src/main/app/shell/Eyeglasses2Icon.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.winlator.cmod.app.shell

import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.PathParser
import androidx.compose.ui.unit.dp

// Material Symbols "eyeglasses_2" (not shipped by compose material-icons). Its 960x960 viewBox is
// offset by -960 in y, so the path lives in a group translated back down by 960.
private const val EYEGLASSES_2_PATH =
"M218-320q-42 0-75.5-27T100-416L71-550l-44 3-7-80q78-7 133.5-10t99.5-3q65 0 105 6t72 21q14 7 " +
"26.5 10t23.5 3q11 0 21.5-3t24.5-9q33-15 76-21.5t114-6.5q46 0 102 3t122 9l-7 79-43-3-30 137q-9 " +
"42-42 68.5T743-320h-89q-42 0-74-25.5T538-411l-27-107h-61l-27 107q-11 41-43 66t-73 25h-89Zm-40-112q3 " +
"14 14 23t25 9h89q14 0 25-8.5t14-21.5l31-121q-27-5-61-6.5t-62-1.5q-23 0-49.5.5T154-556l24 124Zm437 " +
"2q3 13 14 21.5t25 8.5h89q14 0 25-9t14-23l26-125q-20-1-46-1.5t-46-.5q-30 0-66.5 1.5T584-551l31 121Z"

val Eyeglasses2Icon: ImageVector by lazy {
ImageVector.Builder(
name = "Eyeglasses2",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 960f,
viewportHeight = 960f,
).apply {
addGroup(translationY = 960f)
addPath(
pathData = PathParser().parsePathString(EYEGLASSES_2_PATH).toNodes(),
fill = SolidColor(Color.Black),
)
clearGroup()
}.build()
}
139 changes: 139 additions & 0 deletions app/src/main/app/shell/UnifiedActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1090,6 +1090,7 @@ class UnifiedActivity :
if (maybeForwardFrontendLaunch()) return

supportFragmentManager.registerFragmentLifecycleCallbacks(inputControlsFragmentTracker, true)
com.winlator.cmod.runtime.display.GlassesManager.init(this)
bootstrapStartupState()
maybeAutoSignInGoogleOnLaunch()

Expand Down Expand Up @@ -2111,6 +2112,125 @@ class UnifiedActivity :
)
}

@Composable
private fun GlassesSettingsSheet(onDismiss: () -> Unit) {
val gm = com.winlator.cmod.runtime.display.GlassesManager
val settings by gm.settings.collectAsState()
val brightnessMax = gm.brightnessMax()
val volumeMax = gm.volumeMax()
val brightness = if (settings.brightness < 0) brightnessMax else settings.brightness
val volume = if (settings.volume < 0) volumeMax else settings.volume
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(usePlatformDefaultWidth = false),
) {
androidx.compose.material3.Surface(
shape = RoundedCornerShape(24.dp),
color = SurfaceDark,
modifier = Modifier.fillMaxWidth(0.82f),
) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(horizontal = 22.dp, vertical = 18.dp),
verticalArrangement = Arrangement.spacedBy(14.dp),
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Eyeglasses2Icon, contentDescription = null, tint = Accent, modifier = Modifier.size(22.dp))
Spacer(Modifier.width(10.dp))
Text(gm.modelName(), color = TextPrimary, fontSize = 17.sp, fontWeight = FontWeight.SemiBold)
}
Row(horizontalArrangement = Arrangement.spacedBy(24.dp)) {
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(14.dp)) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
GlassesLabel(stringResource(R.string.glasses_panel_refresh))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
listOf(60, 90, 120).forEach { hz ->
val selected = settings.refreshHz == hz
Box(
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(11.dp))
.background(if (selected) Accent else TextSecondary.copy(alpha = 0.12f))
.clickable { gm.setRefreshHz(hz) }
.padding(vertical = 10.dp),
contentAlignment = Alignment.Center,
) {
Text("$hz", color = if (selected) SurfaceDark else TextPrimary,
fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
}
}
}
}
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
GlassesToggleTile(stringResource(R.string.glasses_panel_sunblock),
settings.sunblock, Modifier.weight(1f)) { gm.setSunblock(it) }
GlassesToggleTile(stringResource(R.string.session_drawer_output_3d),
settings.threeD, Modifier.weight(1f)) { gm.set3D(it) }
}
}
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(14.dp)) {
GlassesPercentSlider(stringResource(R.string.session_drawer_output_brightness),
brightness, brightnessMax) { gm.setBrightness(it) }
GlassesPercentSlider(stringResource(R.string.session_drawer_output_volume),
volume, volumeMax) { gm.setVolume(it) }
}
}
}
}
}
}

@Composable
private fun GlassesLabel(text: String) {
Text(text, color = TextSecondary, fontSize = 13.sp, fontWeight = FontWeight.Medium)
}

@Composable
private fun GlassesPercentSlider(label: String, level: Int, max: Int, onChange: (Int) -> Unit) {
val pct = if (max > 0) Math.round(level * 100f / max) else 0
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
GlassesLabel(label)
Text("$pct%", color = Accent, fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
}
androidx.compose.material3.Slider(
value = level.toFloat(),
onValueChange = { onChange(it.roundToInt()) },
valueRange = 0f..max.toFloat(),
steps = (max - 1).coerceAtLeast(0),
colors = androidx.compose.material3.SliderDefaults.colors(
thumbColor = Accent,
activeTrackColor = Accent,
inactiveTrackColor = TextSecondary.copy(alpha = 0.2f),
),
)
}
}

@Composable
private fun GlassesToggleTile(label: String, checked: Boolean, modifier: Modifier = Modifier, onChange: (Boolean) -> Unit) {
Column(
modifier = modifier
.clip(RoundedCornerShape(13.dp))
.background(if (checked) Accent.copy(alpha = 0.16f) else TextSecondary.copy(alpha = 0.08f))
.clickable { onChange(!checked) }
.padding(vertical = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
Text(label, color = TextPrimary, fontSize = 13.sp, fontWeight = FontWeight.Medium)
androidx.compose.material3.Switch(
checked = checked,
onCheckedChange = onChange,
colors = androidx.compose.material3.SwitchDefaults.colors(
checkedThumbColor = Color.White,
checkedTrackColor = Accent,
),
)
}
}

@Composable
private fun TopBar(
tabs: List<TabDef>,
Expand All @@ -2131,6 +2251,8 @@ class UnifiedActivity :
val searchFocusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
val isDownloadsTab = tabs.getOrNull(selectedIdx)?.key == "downloads"
val glassesConnected by com.winlator.cmod.runtime.display.GlassesManager.connected.collectAsState()
var showGlassesPanel by remember { mutableStateOf(false) }

LaunchedEffect(selectedIdx) {
if (isSearchExpanded) {
Expand Down Expand Up @@ -2357,6 +2479,21 @@ class UnifiedActivity :

Spacer(Modifier.width(8.dp))

if (glassesConnected) {
Box(
modifier =
Modifier
.size(44.dp)
.clip(CircleShape)
.background(SurfaceDark)
.clickable { showGlassesPanel = true },
contentAlignment = Alignment.Center,
) {
Icon(Eyeglasses2Icon, contentDescription = "Glasses", tint = Accent, modifier = Modifier.size(24.dp))
}
Spacer(Modifier.width(8.dp))
}

Box(
modifier =
Modifier
Expand All @@ -2377,6 +2514,8 @@ class UnifiedActivity :
}
}

if (showGlassesPanel) GlassesSettingsSheet(onDismiss = { showGlassesPanel = false })

AnimatedVisibility(
visible = isSearchExpanded && !isDownloadsTab,
enter =
Expand Down
1 change: 1 addition & 0 deletions app/src/main/cpp/winlator/vk/vk_dispatch.c
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ bool vkd_load_instance(VkInstance instance) {
LOAD(CmdDraw);
LOAD(CmdPipelineBarrier);
LOAD(CmdCopyBufferToImage);
LOAD(CmdBlitImage);

// Queue
LOAD(QueueSubmit);
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/cpp/winlator/vk/vk_dispatch.h
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ typedef struct VkDispatch {
PFN_vkCmdDraw CmdDraw;
PFN_vkCmdPipelineBarrier CmdPipelineBarrier;
PFN_vkCmdCopyBufferToImage CmdCopyBufferToImage;
PFN_vkCmdBlitImage CmdBlitImage;

// Queue
PFN_vkQueueSubmit QueueSubmit;
Expand Down Expand Up @@ -253,6 +254,7 @@ void vkd_unload(void);
#define vkCmdDraw vkd.CmdDraw
#define vkCmdPipelineBarrier vkd.CmdPipelineBarrier
#define vkCmdCopyBufferToImage vkd.CmdCopyBufferToImage
#define vkCmdBlitImage vkd.CmdBlitImage

#define vkQueueSubmit vkd.QueueSubmit
#define vkQueueWaitIdle vkd.QueueWaitIdle
Expand Down
Loading