fix(screen): compile the MUI mirror out unless it is opted into

setMirror cleared muiOwner outside any guard, so every board without a
TFT failed to compile — the member only exists on the MUI path. The
assignment was redundant anyway: freeSnapshotLocked() releases the pool
and the ownership claim together.

The guards were also drawn on HAS_TFT while the code they protect needs
a device-ui carrying the flush observer, so a color build that never
opts in still carried the rect queue in .bss. Replaces the ad-hoc
HAS_SCREEN_MIRROR && defined(MESHTASTIC_MUI_MIRROR) pairs with a single
derived HAS_MUI_MIRROR next to HAS_SCREEN_MIRROR, and gates the queue,
the pool, the input seam and their declarations on it.

Verified on all three build shapes: t-deck-tft with the opt-in,
picomputer-s3 (TFT, opted out) and heltec-v3 (no TFT).
This commit is contained in:
James Rich committed 2026-11-03 07:39:18 -06:00
1 parent 418843f7b6
commit 2d384a9fa5
6 files changed
+29 -20

No files matched your search

+10
View File
@@ -681,6 +681,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define HAS_SCREEN_MIRROR 0
#endif
// Mirroring MUI (device-ui/LVGL) streams RGB565 dirty rects instead of the
// 1bpp framebuffer, and needs a device-ui carrying the flush observer. Until
// that lands upstream the whole path - queue, pool and input seam - compiles
// out, so a color build that does not opt in carries none of it.
#if HAS_SCREEN_MIRROR && HAS_TFT && defined(MESHTASTIC_MUI_MIRROR)
#define HAS_MUI_MIRROR 1
#else
#define HAS_MUI_MIRROR 0
#endif
#ifndef USE_ETHERNET_DEFAULT
#define USE_ETHERNET_DEFAULT 0
#endif
+7 -8
View File
@@ -40,7 +40,7 @@ void ScreenMirror::freeSnapshotLocked()
}
paletteSig = 0;
paletteCount = 0;
#if HAS_TFT
#if HAS_MUI_MIRROR
if (muiPool) {
memaudit::add("display", -(int32_t)MUI_POOL_BYTES);
free(muiPool);
@@ -64,14 +64,13 @@ void ScreenMirror::setMirror(bool enabled)
// Force an immediate frame so the client doesn't wait for the next
// on-screen change.
oneShot = true;
#if HAS_TFT
#if HAS_MUI_MIRROR
if (muiRefresh)
muiRefresh(); // MUI delivers "a frame" as a full-repaint rect burst
#endif
} else {
oneShot = false;
freeSnapshotLocked();
muiOwner = nullptr;
freeSnapshotLocked(); // also releases the MUI pool and rect ownership
}
if (changed)
LOG_INFO("Screen mirror %s", enabled ? "enabled" : "disabled");
@@ -81,7 +80,7 @@ void ScreenMirror::requestFrame()
{
concurrency::LockGuard g(&lock);
oneShot = true;
#if HAS_TFT
#if HAS_MUI_MIRROR
if (muiRefresh)
muiRefresh();
#endif
@@ -212,7 +211,7 @@ bool ScreenMirror::copyPaletteChunk(uint32_t &clientPaletteSig, uint8_t &clientR
return true;
}
#if HAS_TFT
#if HAS_MUI_MIRROR
void ScreenMirror::onMuiRect(int16_t x, int16_t y, uint16_t w, uint16_t h, const uint16_t *pixels)
{
uint32_t readyId = 0;
@@ -309,7 +308,7 @@ bool ScreenMirror::copyMuiChunkLocked(meshtastic_DisplayFrame &out)
bool ScreenMirror::hasChunkFor(const void *client, uint32_t clientFrameId, uint16_t clientOffset)
{
concurrency::LockGuard g(&lock);
#if HAS_TFT
#if HAS_MUI_MIRROR
// One shared rect cursor means one consumer; another connection simply
// sees no rects rather than stealing half of each frame.
if (muiCount && (muiOwner == nullptr || muiOwner == client))
@@ -321,7 +320,7 @@ bool ScreenMirror::hasChunkFor(const void *client, uint32_t clientFrameId, uint1
bool ScreenMirror::copyChunk(const void *client, uint32_t &clientFrameId, uint16_t &clientOffset, meshtastic_DisplayFrame &out)
{
concurrency::LockGuard g(&lock);
#if HAS_TFT
#if HAS_MUI_MIRROR
// MUI rects drain ahead of (and on MUI builds, instead of) mono snapshots.
// Spike scope: the rect queue has a single consumer, not per-client cursors.
if (muiCount) {
+3 -3
View File
@@ -58,7 +58,7 @@ class ScreenMirror
/// when the client holds the full current palette (or coloring is off).
bool copyPaletteChunk(uint32_t &clientPaletteSig, uint8_t &clientRegionOffset, meshtastic_DisplayPalette &out);
#if HAS_TFT
#if HAS_MUI_MIRROR
/// MUI path: queues one LVGL dirty rect (native little-endian RGB565).
/// Called on the LVGL thread via the device-ui flush observer; copies and returns.
void onMuiRect(int16_t x, int16_t y, uint16_t w, uint16_t h, const uint16_t *pixels);
@@ -101,7 +101,7 @@ class ScreenMirror
uint16_t paletteDefaultOn = 0;
uint16_t paletteDefaultOff = 0;
#if HAS_TFT
#if HAS_MUI_MIRROR
// MUI dirty-rect queue: FIFO rect headers over a linear pixel pool,
// compacted whenever it drains. Spike scope: single consumer.
struct MuiRect {
@@ -131,7 +131,7 @@ class ScreenMirror
extern ScreenMirror screenMirror;
#if HAS_TFT
#if HAS_MUI_MIRROR
/**
* Routes a remote input event straight into device-ui's injection seam.
* MUI builds never construct an InputBroker (Modules.cpp skips it when
+7 -7
View File
@@ -12,7 +12,7 @@
#include "graphics/driver/DisplayDriver.h"
#include "graphics/driver/DisplayDriverConfig.h"
#include "input/InputBroker.h"
#if defined(MESHTASTIC_MUI_MIRROR)
#if HAS_MUI_MIRROR
#include "input/InputDriver.h"
#endif
#include "util/ISpiLock.h"
@@ -321,12 +321,9 @@ class ReentrantSpiLock : public ISpiLock
static ReentrantSpiLock reentrantSpiLock;
#if HAS_SCREEN_MIRROR && defined(MESHTASTIC_MUI_MIRROR)
#if HAS_MUI_MIRROR
namespace graphics
{
// Maps a remote input event onto device-ui's virtual LVGL devices. Note the
// LEFT/RIGHT cross-map: the broker's codes were modeled on LVGL keys but
// those two are swapped.
// Reports MUI's logical panel geometry for DeviceMetadata.display. The BaseUI
// `screen` object does not exist on MUI builds, so the dimensions come from
// LVGL itself (already rotated to the logical orientation).
@@ -347,6 +344,9 @@ bool muiDisplayInfo(uint16_t &width, uint16_t &height, bool &hasTouch)
return width > 0 && height > 0;
}
// Maps a remote input event onto device-ui's virtual LVGL devices. Note the
// LEFT/RIGHT cross-map: the broker's codes were modeled on LVGL keys but
// those two are swapped.
bool muiInjectInputEvent(uint32_t eventCode, uint32_t kbChar, uint32_t touchX, uint32_t touchY)
{
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR)
@@ -412,7 +412,7 @@ void tftSetup(void)
I2CKeyboardScanner::setSecondaryBus(i2cProxy);
#endif
#ifndef ARCH_PORTDUINO
#if HAS_SCREEN_MIRROR && defined(MESHTASTIC_MUI_MIRROR)
#if HAS_MUI_MIRROR
// Must precede DeviceScreen::init: device-ui only builds its virtual input
// devices (and the default focus group they need) when injection is asked for.
InputDriver::enableInjection();
@@ -420,7 +420,7 @@ void tftSetup(void)
deviceScreen = &DeviceScreen::create(reentrantSpiLock);
PacketAPI::create(PacketServer::init());
deviceScreen->init(new PacketClient);
#if HAS_SCREEN_MIRROR && defined(MESHTASTIC_MUI_MIRROR)
#if HAS_MUI_MIRROR
// Stream MUI's dirty rects to local clients (see graphics::ScreenMirror).
// Gated on MESHTASTIC_MUI_MIRROR until the device-ui flush observer merges
// (jamesarich/device-ui screen-mirror-poc); the vendored pin lacks it.
+1 -1
View File
@@ -1394,7 +1394,7 @@ extern meshtastic_DeviceMetadata getDeviceMetadata()
deviceMetadata.has_xeddsa = true;
#endif
#if HAS_TFT && HAS_SCREEN_MIRROR && defined(MESHTASTIC_MUI_MIRROR)
#if HAS_MUI_MIRROR
// MUI owns the panel and leaves `screen` null, so ask LVGL instead.
uint16_t muiW = 0, muiH = 0;
bool muiTouch = false;
+1 -1
View File
@@ -2186,7 +2186,7 @@ void AdminModule::handleSendInputEvent(const meshtastic_AdminMessage_InputEvent
// Wake the device if asleep
powerFSM.trigger(EVENT_INPUT);
#if HAS_SCREEN_MIRROR && defined(MESHTASTIC_MUI_MIRROR)
#if HAS_MUI_MIRROR
// MUI builds never construct an InputBroker (Modules.cpp skips it when
// displaymode is COLOR), so remote input reaches the LVGL UI directly.
if (graphics::muiInjectInputEvent(inputEvent.event_code, inputEvent.kb_char, inputEvent.touch_x, inputEvent.touch_y))