CNA-BUG-176: Setting Game::IsMouseVisible throws PlatformException on the terminal platform when the terminal reports mouse input

CNA snapshot 009d40f5  ·  Known Issues › Current bugs  ·  source links pinned to 009d40f5

✓

Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Nothing on this page was executed unless the Evidence section says so.

Game::setIsMouseVisibleProperty forwards to IPlatformMouse::SetCursorVisible whenever a window and a mouse service exist; the terminal platform supplies a TerminalMouse whenever stdin and stdout are both terminals (TerminalCapabilities::canQuery is isatty(stdin), independent of whether the emulator answers any query), and that mouse always throws.

Identifier
CNA-BUG-176
Category
Bug
Subsystem
Platforms
Status
Open
Verified against
CNA 009d40f5 (009d40f5dd085c4e674d3479675fac84b12b3e0a)
Severity
Medium (a triage suggestion, not a project priority)
Evidence basis
Source-verified: read at TARGET, not executed
Tests touching this area
Yes: see Current tests
Affected contract
Microsoft::Xna::Framework::Game::setIsMouseVisibleProperty(bool); CNA::Platform::IPlatformMouse::SetCursorVisible(bool) on the terminal platform

Expected behaviour

XNA's IsMouseVisible setter never throws, and ported XNA games commonly set it in their constructor. CNA's own test for the property (SettingMouseVisibilityIsSafeWithNoPointerService) states that the setter must work whether or not the platform has a mouse, and expects the terminal to take the same no-pointer path as the headless platform.

Actual behaviour at TARGET

Game::setIsMouseVisibleProperty records the value and then, when GraphicsDevice_ has a platform window and platform_->GetMouse() is non-null, calls SetCursorVisible(value). On the terminal platform the window exists only for the SOFTWARE renderer (the one family that asks for a surface presenter) on a TTY; GraphicsDevice creates it during Game construction. TerminalPlatform::GetMouse() runs EnsureCapabilitiesDetected, which creates a TerminalMouse whenever DetectTerminalCapabilities reports canQuery; canQuery is isatty(stdin) on top of isatty(stdout) and is set before any query is sent, so every interactive terminal session gets the mouse, whether or not the emulator answers the capability queries or supports mouse reporting. TerminalMouse::SetCursorVisible unconditionally throws PlatformException (a terminal cannot control its emulator's graphical pointer). The setter does not catch it, so IsMouseVisible = true in a game's constructor ends construction in an interactive terminal. With stdin or stdout redirected there is no mouse service (and, for stdout, no window), and the call is a no-op. CNA's own plan_platform.md PLAT-49 records that IsMouseVisible was meant to be a determinate no-op under HEADLESS and, later, TERMINAL; PLAT-139 later gave the terminal mouse a service that refuses visibility changes.

Source locations

Evidence

Checked by reading the four files above at 009d40f5. The test's premise (“Headless and, later, terminal both take this path”) no longer holds for a terminal that supports mouse reporting. Not executed; no terminal session was driven.

Independently observed as a separate finding (merged): Game::setIsMouseVisibleProperty forwards a changed value to IPlatformMouse::SetCursorVisible when a window and a mouse service exist; the terminal supplies a mouse when it answers capability queries, and TerminalMouse::SetCursorVisible always throws.

Independent re-verification: Checked by reading Game.cpp, TerminalMouse.cpp, TerminalPlatform.cpp, TerminalCapabilityProbe.cpp, IPlatformMouse.hpp and the tests at 009d40f5; not executed, no terminal session was driven. TerminalCapabilityProbeTests (ATerminalThatAnswersNothingLeavesEveryQueriedCapabilityFalse) pins that canQuery stays true when the terminal answers nothing. The test's premise (headless and, later, terminal both take the no-pointer path) holds only when stdin or stdout is not a terminal, as under ctest, which is why it passes. TerminalMouseTests pins the platform-level refusal, so what is missing is the caller's handling of it, not the refusal itself.

Focused reproduction

Illustrative: build with the terminal platform and a renderer that creates a window, run in a terminal that answers CNA's capability queries (for example one with SGR mouse reporting), and construct a game whose constructor calls setIsMouseVisibleProperty(true); construction ends with CNA::Platform::PlatformException from TerminalMouse::SetCursorVisible.

Current tests

The three mouse-visibility tests in GamePlatformTimingTests.cpp run on the build's default platform; no configuration runs them against a terminal that reports a mouse.

Regression test

A runtime test on the terminal platform with a scripted terminal that answers the capability query (or a platform decorator returning a throwing mouse): setting IsMouseVisible must not throw and must still record the value.

Blast radius

Games on the terminal platform in terminals that answer CNA's capability queries, whenever they change IsMouseVisible; other platforms (their mouse services implement the call) and terminals without query support (no mouse service) are unaffected.

Workaround

On the terminal platform, do not set IsMouseVisible, or guard the call with a try/catch (const CNA::Platform::PlatformException&).

The same subject is explained at several altitudes. These are the neighbouring pages at each one.

User guide
Input: mouse
Known issues
Bug index