#define _POSIX_C_SOURCE 200809L #include "h618_hub75.h" #include #include #include #include #include #include static double monotonic_seconds(void) { struct timespec value; clock_gettime(CLOCK_MONOTONIC, &value); return (double)value.tv_sec + (double)value.tv_nsec / 1000000000.0; } static int integer_argument(const char *value, const char *name) { char *end = NULL; long parsed = strtol(value, &end, 10); if (value[0] == '\0' || end == NULL || *end != '\0' || parsed < 0 || parsed > 1000000) { fprintf(stderr, "invalid %s: %s\n", name, value); exit(2); } return (int)parsed; } static int hex_nibble(char value) { if (value >= '0' && value <= '9') return value - '0'; if (value >= 'a' && value <= 'f') return value - 'a' + 10; if (value >= 'A' && value <= 'F') return value - 'A' + 10; return -1; } static void color_argument(const char *value, uint8_t output[3]) { if (strlen(value) != 6) { fprintf(stderr, "invalid color: %s (expected RRGGBB)\n", value); exit(2); } for (unsigned channel = 0; channel < 3; ++channel) { int high = hex_nibble(value[channel * 2]); int low = hex_nibble(value[channel * 2 + 1]); if (high < 0 || low < 0) { fprintf(stderr, "invalid color: %s (expected RRGGBB)\n", value); exit(2); } output[channel] = (uint8_t)((high << 4) | low); } } int main(int argc, char **argv) { int duration = 10; int refresh_rate = 100; int brightness = 40; int use_dev_mem = 0; int use_static_color = 0; uint8_t static_color[3] = {0}; for (int index = 1; index < argc; ++index) { if (strcmp(argv[index], "--duration") == 0 && index + 1 < argc) { duration = integer_argument(argv[++index], "duration"); } else if (strcmp(argv[index], "--refresh-rate") == 0 && index + 1 < argc) { refresh_rate = integer_argument(argv[++index], "refresh rate"); } else if (strcmp(argv[index], "--brightness") == 0 && index + 1 < argc) { brightness = integer_argument(argv[++index], "brightness"); } else if (strcmp(argv[index], "--dev-mem") == 0) { use_dev_mem = 1; } else if (strcmp(argv[index], "--color") == 0 && index + 1 < argc) { color_argument(argv[++index], static_color); use_static_color = 1; } else { fprintf(stderr, "usage: %s [--duration N] [--refresh-rate N] [--brightness N] [--color RRGGBB] [--dev-mem]\n", argv[0]); return 2; } } if (duration < 1) { fputs("duration must be at least one second\n", stderr); return 2; } hub75_config config = { .brightness = brightness, .refresh_rate_hz = refresh_rate, .cpu_affinity = 3, .realtime_priority = 50, .use_dev_mem = use_dev_mem, }; char error[512] = ""; hub75_context *context = hub75_create(&config, error, sizeof(error)); if (context == NULL) { fprintf(stderr, "create failed: %s\n", error); return 1; } if (hub75_start(context) != 0) { fputs("start failed\n", stderr); hub75_destroy(context); return 1; } uint8_t frame[HUB75_FRAME_BYTES]; if (use_static_color) { for (unsigned pixel = 0; pixel < HUB75_WIDTH * HUB75_HEIGHT; ++pixel) { memcpy(frame + pixel * 3u, static_color, sizeof(static_color)); } if (hub75_submit_rgb(context, frame, sizeof(frame)) != 0) { fputs("initial static submit failed\n", stderr); hub75_destroy(context); return 1; } } double started = monotonic_seconds(); unsigned submitted = 0; while (monotonic_seconds() - started < duration) { if (!use_static_color) { for (unsigned pixel = 0; pixel < HUB75_WIDTH * HUB75_HEIGHT; ++pixel) { frame[pixel * 3] = (uint8_t)(pixel + submitted); frame[pixel * 3 + 1] = (uint8_t)(pixel * 3u + submitted); frame[pixel * 3 + 2] = (uint8_t)(pixel * 7u + submitted); } if (hub75_submit_rgb(context, frame, sizeof(frame)) != 0) { fputs("submit failed\n", stderr); hub75_destroy(context); return 1; } ++submitted; } struct timespec delay = {.tv_sec = 0, .tv_nsec = 20000000}; nanosleep(&delay, NULL); } hub75_stats stats; if (hub75_get_stats(context, &stats) != 0) { fputs("stats failed\n", stderr); hub75_destroy(context); return 1; } hub75_destroy(context); int expected_scans = hub75_scans_per_frame_for_brightness(brightness); double miss_rate = stats.completed_frames == 0 ? 100.0 : (double)stats.deadline_misses * 100.0 / (double)stats.completed_frames; printf( "{\"mode\":\"%s\",\"configured_hz\":%d,\"actual_hz\":%.3f," "\"panel_scan_hz\":%.3f,\"completed_frames\":%llu," "\"completed_scans\":%llu,\"scans_per_frame\":%d," "\"deadline_misses\":%llu," "\"deadline_miss_percent\":%.6f,\"buffer_swaps\":%llu," "\"submitted_frames\":%llu,\"oe_timing_backend\":\"%s\"," "\"oe_pulse_faults\":%llu,\"oe_forced_blanks\":%llu," "\"max_programmed_oe_ns\":%llu,\"oe_timing_error\":\"%s\"," "\"cpu_affinity_active\":%s," "\"realtime_priority_active\":%s,\"memory_locked\":%s," "\"governor\":\"%s\",\"affinity_error\":\"%s\"," "\"realtime_error\":\"%s\",\"memory_lock_error\":\"%s\"," "\"last_error\":\"%s\"}\n", use_dev_mem ? "dev-mem" : "fake-registers", refresh_rate, stats.actual_refresh_rate_hz, stats.panel_scan_rate_hz, (unsigned long long)stats.completed_frames, (unsigned long long)stats.completed_scans, stats.scans_per_frame, (unsigned long long)stats.deadline_misses, miss_rate, (unsigned long long)stats.buffer_swaps, (unsigned long long)stats.submitted_frames, stats.oe_timing_backend, (unsigned long long)stats.oe_pulse_faults, (unsigned long long)stats.oe_forced_blanks, (unsigned long long)stats.max_programmed_oe_ns, stats.oe_timing_error, stats.cpu_affinity_active ? "true" : "false", stats.realtime_priority_active ? "true" : "false", stats.memory_locked ? "true" : "false", stats.governor, stats.affinity_error, stats.realtime_error, stats.memory_lock_error, stats.last_error ); if ( stats.completed_frames == 0 || stats.actual_refresh_rate_hz <= 0.0 || stats.panel_scan_rate_hz <= 0.0 || stats.scans_per_frame != expected_scans || stats.completed_scans != stats.completed_frames * (uint64_t)expected_scans || stats.oe_pulse_faults != 0 || strcmp(stats.oe_timing_backend, HUB75_OE_TIMING_BACKEND) != 0 ) return 1; return 0; }