Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ target_link_libraries(test_core PRIVATE mx_core)
mx_link_system(test_core)
add_test(NAME test_core COMMAND test_core)

if(NOT WIN32)
add_executable(test_open_posix tests/test_open_posix.c)
target_link_libraries(test_open_posix PRIVATE mx_core)
mx_link_system(test_open_posix)
add_test(NAME test_open_posix COMMAND test_open_posix)
endif()

if(WIN32)
# external_renderer_ipc.dll: the compatibility shell for MaaCore's MuMu path.
add_library(external_renderer_ipc SHARED shim/mumu_shim.c)
Expand Down
7 changes: 6 additions & 1 deletion include/modavd_extras.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ enum {

/* modavd_open_params.flags */
enum {
/* Do not ask the emulator console to start frame sharing when it is off. */
/* Only open the shared mapping, without contacting the console. The caller
* must ensure it belongs to the current emulator and sharing is active. */
MODAVD_OPEN_NO_START = 1u << 0,
};

Expand Down Expand Up @@ -129,6 +130,10 @@ MODAVD_API void modavd_set_log(modavd_log_fn fn, void* user);
MODAVD_API int modavd_parse_console_port(const char* adb_address);

/* Connects to an instance. On success *out is a handle for modavd_close().
* On POSIX, unless NO_START is set, confirms sharing through the console before
* opening the mapping: named regions can survive an emulator restart. Console
* auth/command failures return MODAVD_E_IO, even if an old mapping exists.
* The first frame may not have arrived yet; capture then returns NOT_READY.
* MODAVD_E_NOT_FOUND: frame sharing is off and nothing answers on the console port.
* MODAVD_E_NOT_READY: the console answered (or MODAVD_OPEN_NO_START is set) but
* frame sharing did not come up in time. */
Expand Down
17 changes: 16 additions & 1 deletion src/core.c
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,26 @@ int modavd_open(const modavd_open_params* params, modavd_handle* out)
}
int timeout_ms = params->timeout_ms > 0 ? params->timeout_ms : MX_DEFAULT_TIMEOUT_MS;

#ifndef _WIN32
/* POSIX names can outlive their producer. Confirm sharing before opening
* the name, since the new emulator may replace the stale object on start.
* Repeated starts reuse the active stream. NO_START trusts the mapping. */
if (!(params->flags & MODAVD_OPEN_NO_START)) {
int console = mx_console_command(port, "screenrecord webrtc start", 3000);
if (console != MODAVD_OK) {
return console;
}
}
#endif
mx_shm shm = { 0 };
int rc = mx_shm_open(port, &shm);
if (rc == MODAVD_E_NOT_READY && !(params->flags & MODAVD_OPEN_NO_START)) {
#ifdef _WIN32
int console = mx_console_command(port, "screenrecord webrtc start", 3000);
if (console == MODAVD_E_NOT_FOUND) {
return MODAVD_E_NOT_FOUND;
}
#endif
for (int waited = 0; rc == MODAVD_E_NOT_READY && waited < timeout_ms; waited += 50) {
sleep_ms(50);
rc = mx_shm_open(port, &shm);
Expand Down Expand Up @@ -296,7 +309,9 @@ int mx_copy_frame(const mx_shm* shm, uint32_t format, uint32_t flags, int simd,
info->format = format;
info->frame_number = vi->frame_number;
info->timestamp_us = vi->ts_us;
if (w == 0 || h == 0) {
/* Dimensions are set before the first frame is published. The counter
* can wrap to zero, and a published static frame need not advance. */
if (w == 0 || h == 0 || info->timestamp_us == 0) {
return MODAVD_E_NOT_READY;
}
if (MX_HEADER_SIZE + (uint64_t)w * h * 4 > shm->size) {
Expand Down
50 changes: 50 additions & 0 deletions tests/test_core.c
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,55 @@ static void test_capture_from_fake_frame(void)
fake_frame_destroy(&f);
}

static void test_capture_before_first_frame(void)
{
fake_frame f;
if (fake_frame_create(&f, 65012, 8, 4, 8, 4) != 0) {
++g_failures;
return;
}
modavd_open_params p = { 0 };
p.size = sizeof(p);
p.console_port = f.port;
p.flags = MODAVD_OPEN_NO_START;
modavd_handle h = NULL;
CHECK_EQ(modavd_open(&p, &h), MODAVD_OK);
if (h) {
mx_video_info* vi = (mx_video_info*)f.view;
vi->frame_number = 0;
vi->ts_us = 0;
uint8_t buf[8 * 4 * 4];
modavd_frame_info info = { 0 };
info.size = sizeof(info);
for (uint32_t format = MODAVD_FMT_BGRA; format <= MODAVD_FMT_RGBA; ++format) {
CHECK_EQ(modavd_capture(h, 0, format, 0, NULL, 0, &info), MODAVD_E_NOT_READY);
memset(buf, 0xA5, sizeof(buf));
CHECK_EQ(modavd_capture(h, 0, format, 0, buf, sizeof(buf), &info), MODAVD_E_NOT_READY);
for (size_t i = 0; i < sizeof(buf); ++i) {
CHECK_EQ(buf[i], 0xA5);
}
}

/* A counter may wrap to zero. A published, unchanged black frame is valid. */
vi->ts_us = 123456789ull;
memset(f.view + MX_HEADER_SIZE, 0, 8 * 4 * 4);
for (int i = 0; i < 2; ++i) {
CHECK_EQ(modavd_capture(h, 0, MODAVD_FMT_BGRA, 0, buf, sizeof(buf), &info), MODAVD_OK);
CHECK_EQ(info.frame_number, 0);
CHECK_EQ(info.timestamp_us, 123456789ull);
for (size_t j = 0; j < sizeof(buf); ++j) {
CHECK_EQ(buf[j], 0);
}
}
/* The counter alone does not mean the timestamp was published yet. */
vi->frame_number = 1;
vi->ts_us = 0;
CHECK_EQ(modavd_capture(h, 0, MODAVD_FMT_BGRA, 0, buf, sizeof(buf), &info), MODAVD_E_NOT_READY);
modavd_close(h);
}
fake_frame_destroy(&f);
}

/* The emulator writes without a lock. The hook publishes a frame after a copy,
* as if the emulator wrote one meanwhile. */
typedef struct publish_state {
Expand Down Expand Up @@ -416,6 +465,7 @@ int main(void)
test_version_and_errors();
test_open_errors();
test_capture_from_fake_frame();
test_capture_before_first_frame();
test_capture_new_frame_mid_copy();
test_capture_odd_sizes();
test_convert_row();
Expand Down
102 changes: 102 additions & 0 deletions tests/test_open_posix.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* POSIX open tests with real shared memory and a controlled console response.
*
* Copyright 2026 HarcoChen
* SPDX-License-Identifier: LGPL-3.0-or-later
*/
#include <stdio.h>

#include "fake_frame.h"

static int failures;
static int console_calls;
static int console_result;
static fake_frame* replacement;

#define CHECK(condition) \
do { \
if (!(condition)) { \
fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, #condition); \
++failures; \
} \
} while (0)

/* This definition replaces console.c from the static archive. No network or
* auth token is needed; core.c and shm.c run through the public entry points. */
int mx_console_command(int port, const char* command, int timeout_ms)
{
++console_calls;
CHECK(port == 65013);
CHECK(strcmp(command, "screenrecord webrtc start") == 0);
CHECK(timeout_ms > 0);
if (replacement) {
CHECK(fake_frame_create(replacement, port, 8, 4, 8, 4) == 0);
}
return console_result;
}

int main(void)
{
fake_frame stale;
if (fake_frame_create(&stale, 65013, 8, 4, 8, 4) != 0) {
return 1;
}
fake_frame_set_size(&stale, 4, 2);
modavd_open_params p = { 0 };
p.size = sizeof(p);
p.console_port = stale.port;
p.timeout_ms = 100;
modavd_handle h = NULL;

/* A valid old frame must not hide an absent or inaccessible producer. */
console_result = MODAVD_E_NOT_FOUND;
CHECK(modavd_open(&p, &h) == MODAVD_E_NOT_FOUND);
CHECK(h == NULL);
modavd_close(h);
console_result = MODAVD_E_IO;
CHECK(modavd_open(&p, &h) == MODAVD_E_IO);
CHECK(h == NULL);
modavd_close(h);
CHECK(console_calls == 2);

/* NO_START explicitly trusts the supplied mapping, without a console. */
p.flags = MODAVD_OPEN_NO_START;
CHECK(modavd_open(&p, &h) == MODAVD_OK);
CHECK(console_calls == 2);
modavd_close(h);

/* The new producer replaces the old name when starting. Open after that
* command so that the handle sees the new 8x4 frame, not the old 4x2 one. */
fake_frame current = { 0 };
replacement = &current;
console_result = MODAVD_OK;
p.flags = 0;
CHECK(modavd_open(&p, &h) == MODAVD_OK);
CHECK(console_calls == 3);
CHECK(h != NULL);
replacement = NULL;
modavd_handle second = NULL;
CHECK(modavd_open(&p, &second) == MODAVD_OK);
CHECK(console_calls == 4);
CHECK(second != NULL);
modavd_frame_info info = { 0 };
info.size = sizeof(info);
uint8_t buf[8 * 4 * 4];
if (h && second) {
CHECK(modavd_capture(h, 0, MODAVD_FMT_BGRA, 0, buf, sizeof(buf), &info) == MODAVD_OK);
CHECK(info.width == 8 && info.height == 4);
CHECK(modavd_capture(second, 0, MODAVD_FMT_BGRA, 0, buf, sizeof(buf), &info) == MODAVD_OK);
CHECK(info.width == 8 && info.height == 4);
modavd_close(second);
second = NULL;
/* Closing another reader must not stop a static, already published frame. */
CHECK(modavd_capture(h, 0, MODAVD_FMT_BGRA, 0, buf, sizeof(buf), &info) == MODAVD_OK);
}
modavd_close(second);
modavd_close(h);
if (current.view) {
fake_frame_destroy(&current);
}
fake_frame_destroy(&stale);
return failures ? 1 : 0;
}
Loading