Skip to content
Merged
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
124 changes: 115 additions & 9 deletions src/core.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ static void sleep_ms(int ms)
}
#endif

#if defined(__x86_64__) || defined(__i386__) || defined(_M_X64) || defined(_M_IX86)
#define MX_X86 1
#include <tmmintrin.h>
#if defined(_MSC_VER)
#include <intrin.h>
#else
#include <cpuid.h>
#endif
#elif defined(__aarch64__) || defined(_M_ARM64)
#define MX_NEON 1
#include <arm_neon.h>
#endif

#ifndef MODAVD_EXTRAS_VERSION
#define MODAVD_EXTRAS_VERSION "0.0.0"
#endif
Expand Down Expand Up @@ -112,6 +125,7 @@ int modavd_open(const modavd_open_params* params, modavd_handle* out)
return MODAVD_E_IO;
}
inst->console_port = port;
inst->simd = mx_cpu_simd();
inst->shm = shm;
const volatile mx_video_info* vi = (const volatile mx_video_info*)shm.view;
mx_log(MODAVD_LOG_INFO, "open port=%d %ux%u fps=%u frame=%u size=%zu client=%s", port, vi->width, vi->height,
Expand Down Expand Up @@ -143,7 +157,90 @@ static int bytes_per_pixel(uint32_t format)
}
}

static void convert_row(const uint8_t* src, uint8_t* dst, uint32_t width, uint32_t format)
#if defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
#error "the pixel conversion below assumes a little-endian CPU"
#endif

/* BGRA -> BGR, portable: four pixels (16 bytes) in, three little-endian words (12 bytes) out,
* then one pixel at a time for the last 0-3. */
static void bgr_row_portable(const uint8_t* src, uint8_t* dst, uint32_t width)
{
uint32_t x = 0;
for (; x + 4 <= width; x += 4, src += 16, dst += 12) {
uint32_t p[4], o[3];
memcpy(p, src, sizeof(p));
o[0] = (p[0] & 0x00FFFFFFu) | (p[1] << 24); /* B0 G0 R0 B1 */
o[1] = ((p[1] >> 8) & 0x0000FFFFu) | (p[2] << 16); /* G1 R1 B2 G2 */
o[2] = ((p[2] >> 16) & 0x000000FFu) | (p[3] << 8); /* R2 B3 G3 R3 */
memcpy(dst, o, sizeof(o));
}
for (; x < width; ++x, src += 4, dst += 3) {
dst[0] = src[0];
dst[1] = src[1];
dst[2] = src[2];
}
}

#if MX_X86
/* BGRA -> BGR, 16 pixels at a time: pshufb packs each 16-byte load to 12 bytes,
* then shifts and ORs splice four of those into three 16-byte stores.
* Returns how many pixels it converted (a multiple of 16). */
#if defined(__GNUC__) || defined(__clang__)
__attribute__((target("ssse3")))
#endif
static uint32_t bgr_row_ssse3(const uint8_t* src, uint8_t* dst, uint32_t width)
{
const __m128i pack = _mm_setr_epi8(0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, -1, -1, -1, -1);
uint32_t x = 0;
for (; x + 16 <= width; x += 16, src += 64, dst += 48) {
__m128i a = _mm_shuffle_epi8(_mm_loadu_si128((const __m128i*)(const void*)src), pack);
__m128i b = _mm_shuffle_epi8(_mm_loadu_si128((const __m128i*)(const void*)(src + 16)), pack);
__m128i c = _mm_shuffle_epi8(_mm_loadu_si128((const __m128i*)(const void*)(src + 32)), pack);
__m128i d = _mm_shuffle_epi8(_mm_loadu_si128((const __m128i*)(const void*)(src + 48)), pack);
_mm_storeu_si128((__m128i*)(void*)dst, _mm_or_si128(a, _mm_slli_si128(b, 12)));
_mm_storeu_si128((__m128i*)(void*)(dst + 16), _mm_or_si128(_mm_srli_si128(b, 4), _mm_slli_si128(c, 8)));
_mm_storeu_si128((__m128i*)(void*)(dst + 32), _mm_or_si128(_mm_srli_si128(c, 8), _mm_slli_si128(d, 4)));
}
return x;
}
#elif MX_NEON
/* BGRA -> BGR, 16 pixels at a time: split the channels, store three of them interleaved. */
static uint32_t bgr_row_neon(const uint8_t* src, uint8_t* dst, uint32_t width)
{
uint32_t x = 0;
for (; x + 16 <= width; x += 16, src += 64, dst += 48) {
uint8x16x4_t p = vld4q_u8(src);
uint8x16x3_t o;
o.val[0] = p.val[0];
o.val[1] = p.val[1];
o.val[2] = p.val[2];
vst3q_u8(dst, o);
}
return x;
}
#endif

int mx_cpu_simd(void)
{
#if MX_X86
#if defined(__SSSE3__) || defined(__AVX__)
return 1;
#elif defined(_MSC_VER)
int r[4];
__cpuid(r, 1);
return (r[2] >> 9) & 1; /* CPUID.1:ECX.SSSE3 */
#else
unsigned a, b, c, d;
return __get_cpuid(1, &a, &b, &c, &d) && ((c >> 9) & 1);
#endif
#elif MX_NEON
return 1; /* part of every ARM64 CPU */
#else
return 0;
#endif
}

void mx_convert_row(const uint8_t* src, uint8_t* dst, uint32_t width, uint32_t format, int simd)
{
switch (format) {
case MODAVD_FMT_BGRA:
Expand All @@ -159,19 +256,28 @@ static void convert_row(const uint8_t* src, uint8_t* dst, uint32_t width, uint32
memcpy(dst + (size_t)x * 4, &p, 4);
}
break;
case MODAVD_FMT_BGR:
for (uint32_t x = 0; x < width; ++x, src += 4, dst += 3) {
dst[0] = src[0];
dst[1] = src[1];
dst[2] = src[2];
case MODAVD_FMT_BGR: {
uint32_t done = 0;
#if MX_X86
if (simd) {
done = bgr_row_ssse3(src, dst, width);
}
#elif MX_NEON
if (simd) {
done = bgr_row_neon(src, dst, width);
}
#else
(void)simd;
#endif
bgr_row_portable(src + (size_t)done * 4, dst + (size_t)done * 3, width - done);
break;
}
default:
break;
}
}

int mx_copy_frame(const mx_shm* shm, uint32_t format, uint32_t flags, void* buf, size_t buf_size,
int mx_copy_frame(const mx_shm* shm, uint32_t format, uint32_t flags, int simd, void* buf, size_t buf_size,
modavd_frame_info* info)
{
int bpp = bytes_per_pixel(format);
Expand Down Expand Up @@ -208,7 +314,7 @@ int mx_copy_frame(const mx_shm* shm, uint32_t format, uint32_t flags, void* buf,
uint32_t before = vi->frame_number;
for (uint32_t y = 0; y < h; ++y) {
uint32_t sy = (flags & MODAVD_CAPTURE_BOTTOM_UP) ? h - 1 - y : y;
convert_row(src + (size_t)sy * src_stride, dst + (size_t)y * (size_t)info->stride, w, format);
mx_convert_row(src + (size_t)sy * src_stride, dst + (size_t)y * (size_t)info->stride, w, format, simd);
}
uint32_t after = vi->frame_number;
info->frame_number = after;
Expand All @@ -229,7 +335,7 @@ int modavd_capture(modavd_handle h, int display, uint32_t format, uint32_t flags
if (display != 0) {
return MODAVD_E_UNSUPPORTED;
}
return mx_copy_frame(&h->shm, format, flags, buf, buf_size, info);
return mx_copy_frame(&h->shm, format, flags, h->simd, buf, buf_size, info);
}

/* Input arrives in the next version. */
Expand Down
12 changes: 11 additions & 1 deletion src/mx_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,21 @@ int mx_console_command(int port, const char* command, int timeout_ms);
/* ---- core.c ---- */
struct modavd_instance {
int console_port;
int simd; /* mx_cpu_simd() at open */
mx_shm shm;
};

/* 1 when this CPU runs the vector row conversion (SSSE3 on x86, NEON on
* ARM64), else 0. */
int mx_cpu_simd(void);

/* Converts one row of width BGRA pixels into format (MODAVD_FMT_*), writing
* exactly width * bytes-per-pixel bytes. src and dst need no alignment.
* simd: the value of mx_cpu_simd(), or 0 for the portable code only. */
void mx_convert_row(const uint8_t* src, uint8_t* dst, uint32_t width, uint32_t format, int simd);

/* Copies one frame; the work behind modavd_capture(). */
int mx_copy_frame(const mx_shm* shm, uint32_t format, uint32_t flags, void* buf, size_t buf_size,
int mx_copy_frame(const mx_shm* shm, uint32_t format, uint32_t flags, int simd, void* buf, size_t buf_size,
modavd_frame_info* info);

#ifdef __cplusplus
Expand Down
118 changes: 117 additions & 1 deletion tests/test_core.c
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ static uint8_t expected_channel(uint32_t format, int channel, uint32_t x, uint32
switch (format) {
case MODAVD_FMT_BGRA: return (uint8_t[]){ b, g, r, 0xFF }[channel];
case MODAVD_FMT_RGBA: return (uint8_t[]){ r, g, b, 0xFF }[channel];
default: return (uint8_t[]){ b, g, r }[channel];
default: return (uint8_t[]){ b, g, r, 0 }[channel]; /* 4 entries: GCC -O3 cannot tell channel < 3 here */
}
}

Expand Down Expand Up @@ -204,6 +204,120 @@ static void test_capture_from_fake_frame(void)
fake_frame_destroy(&f);
}

/* Same frame through the public call at sizes that leave every possible tail after the
* 16- and 4-pixel blocks, top-down and bottom-up. */
static void test_capture_odd_sizes(void)
{
static const uint32_t sizes[][2] = { { 1, 1 }, { 1, 5 }, { 3, 4 }, { 15, 3 }, { 17, 5 }, { 33, 2 }, { 1281, 3 } };
for (size_t i = 0; i < sizeof(sizes) / sizeof(sizes[0]); ++i) {
const int port = 65003 + (int)i;
const uint32_t w = sizes[i][0], hgt = sizes[i][1];
fake_frame f;
if (fake_frame_create(&f, port, w, hgt, w, hgt) != 0) {
fprintf(stderr, "cannot create the fake frame %ux%u\n", w, hgt);
++g_failures;
continue;
}
modavd_open_params p;
memset(&p, 0, sizeof(p));
p.size = sizeof(p);
p.console_port = port;
p.flags = MODAVD_OPEN_NO_START;
modavd_handle h = NULL;
CHECK_EQ(modavd_open(&p, &h), MODAVD_OK);
if (h) {
static const uint32_t all_flags[] = { 0, MODAVD_CAPTURE_BOTTOM_UP };
for (size_t fi = 0; fi < 2; ++fi) {
check_capture(h, MODAVD_FMT_BGR, 3, all_flags[fi], w, hgt);
check_capture(h, MODAVD_FMT_RGBA, 4, all_flags[fi], w, hgt);
check_capture(h, MODAVD_FMT_BGRA, 4, all_flags[fi], w, hgt);
}
modavd_close(h);
}
fake_frame_destroy(&f);
}
}

/* One pixel of the reference: what mx_convert_row must write for BGRA pixel s. */
static void reference_pixel(const uint8_t* s, uint32_t format, uint8_t* out)
{
switch (format) {
case MODAVD_FMT_RGBA:
out[0] = s[2], out[1] = s[1], out[2] = s[0], out[3] = s[3];
break;
case MODAVD_FMT_BGR:
out[0] = s[0], out[1] = s[1], out[2] = s[2];
break;
default:
memcpy(out, s, 4);
break;
}
}

/* Row conversion against the per-pixel reference: every width up to a few blocks plus
* real screen widths, misaligned source and destination, the alpha byte not 0xFF, and
* guard bytes on both sides of the row to catch a block writing past its end. Runs the
* portable code and, where the CPU has it, the vector code. */
static void test_convert_row(void)
{
const size_t GUARD = 64, MAX_W = 1921;
static const uint32_t extra_widths[] = { 255, 256, 257, 719, 720, 1279, 1280, 1281, 1919, 1920, 1921 };
static const uint32_t formats[][2] = { { MODAVD_FMT_BGR, 3 }, { MODAVD_FMT_RGBA, 4 }, { MODAVD_FMT_BGRA, 4 } };
uint8_t* src = (uint8_t*)malloc(MAX_W * 4 + 4);
uint8_t* dst = (uint8_t*)malloc(GUARD + MAX_W * 4 + 4 + GUARD);
uint8_t* want = (uint8_t*)malloc(MAX_W * 4);
if (!src || !dst || !want) {
++g_failures;
free(src), free(dst), free(want);
return;
}
uint32_t seed = 12345;
for (size_t i = 0; i < MAX_W * 4 + 4; ++i) {
seed = seed * 1103515245u + 12345u;
src[i] = (uint8_t)(seed >> 16);
}

int simd_levels[2] = { 0, mx_cpu_simd() };
int n_levels = simd_levels[1] ? 2 : 1;
printf("test_core: row conversion with portable code%s\n", n_levels == 2 ? " and the vector path" : " only");
int bad = 0;
for (int lv = 0; lv < n_levels && !bad; ++lv) {
for (size_t fi = 0; fi < sizeof(formats) / sizeof(formats[0]) && !bad; ++fi) {
const uint32_t format = formats[fi][0], bpp = formats[fi][1];
for (uint32_t wi = 0; wi < 68 + sizeof(extra_widths) / sizeof(extra_widths[0]) && !bad; ++wi) {
const uint32_t w = wi < 68 ? wi : extra_widths[wi - 68];
for (uint32_t so = 0; so < 4 && !bad; ++so) {
for (uint32_t d_off = 0; d_off < 4 && !bad; ++d_off) {
const uint8_t* s = src + so;
for (uint32_t x = 0; x < w; ++x) {
reference_pixel(s + (size_t)x * 4, format, want + (size_t)x * bpp);
}
size_t row = (size_t)w * bpp, total = GUARD + MAX_W * 4 + 4 + GUARD;
memset(dst, 0xA5, total);
uint8_t* d = dst + GUARD + d_off;
mx_convert_row(s, d, w, format, simd_levels[lv]);
if (memcmp(d, want, row) != 0) {
fprintf(stderr, "simd %d format %u width %u offsets %u/%u: row differs\n",
simd_levels[lv], format, w, so, d_off);
bad = 1;
}
for (size_t i = 0; i < total && !bad; ++i) {
int inside = i >= (size_t)(d - dst) && i < (size_t)(d - dst) + row;
if (!inside && dst[i] != 0xA5) {
fprintf(stderr, "simd %d format %u width %u offsets %u/%u: wrote outside the row\n",
simd_levels[lv], format, w, so, d_off);
bad = 1;
}
}
}
}
}
}
}
CHECK(!bad);
free(src), free(dst), free(want);
}

static int g_log_lines;
static void count_log(int level, const char* msg, void* user)
{
Expand Down Expand Up @@ -233,6 +347,8 @@ int main(void)
test_version_and_errors();
test_open_errors();
test_capture_from_fake_frame();
test_capture_odd_sizes();
test_convert_row();
test_log_callback();
if (g_failures) {
fprintf(stderr, "%d check(s) failed\n", g_failures);
Expand Down
Loading