diff --git a/src/main/d3d9-monitor-check/Module.mk b/src/main/d3d9-monitor-check/Module.mk new file mode 100644 index 0000000..ab06e3e --- /dev/null +++ b/src/main/d3d9-monitor-check/Module.mk @@ -0,0 +1,22 @@ +exes += d3d9-monitor-check \ + +ldflags_d3d9-monitor-check := \ + -ld3d9 \ + -ldwmapi \ + -lgdi32 \ + -ld3dx9 \ + +libs_d3d9-monitor-check := \ + util \ + +src_d3d9-monitor-check := \ + cmdline.c \ + font.c \ + gfx.c \ + input.c \ + interactive.c \ + main.c \ + menu.c \ + refresh-rate-test.c \ + response-time-test.c \ + vsync-test.c \ No newline at end of file diff --git a/src/main/d3d9-monitor-check/cmdline.c b/src/main/d3d9-monitor-check/cmdline.c new file mode 100644 index 0000000..628b306 --- /dev/null +++ b/src/main/d3d9-monitor-check/cmdline.c @@ -0,0 +1,534 @@ +#include +#include + +#include "d3d9-monitor-check/gfx.h" +#include "d3d9-monitor-check/input.h" +#include "d3d9-monitor-check/print-console.h" +#include "d3d9-monitor-check/refresh-rate-test.h" +#include "d3d9-monitor-check/response-time-test.h" +#include "d3d9-monitor-check/vsync-test.h" + +static void _print_synopsis() +{ + printfln_err("D3D9 monitor check tool command line mode"); + printfln_err(""); + printfln_err("Usage:"); + printfln_err(" d3d9-monitor-check cmdline "); + printfln_err(""); + printfln_err("Available commands:"); + printfln_err(" adapter: Query adapter information"); + printfln_err(" modes: Query adapter modes. Use this to get supported mandatory parameters for width, height and refresh rate for the tests."); + printfln_err(""); + printfln_err(" refresh-rate-test [--warm-up-secs n] [--total-secs n] [--results-timeout-secs n] [--windowed] [--vsync-off]: Run a display refresh rate test (i.e. IIDX monitor check)."); + printfln_err(" width: Width of the rendering resolution to run the test at"); + printfln_err(" height: Height of the rendering resolution to run the test at"); + printfln_err(" refresh_rate: Target refresh rate to run the test at"); + printfln_err(" warm-up-secs: Optional. Number of seconds to warm-up before executing the main run that counts towards the measurement results"); + printfln_err(" total-secs: Optional. Total number of seconds to run the test for that count towards the measurement results"); + printfln_err(" results-timeout-secs: Optional. Number of seconds to display final result after the test before exiting"); + printfln_err(" windowed: Optional. Run the test in windowed mode (not recommended)"); + printfln_err(" vsync-off: Optional. Run the test with vsync off (not recommended)"); + printfln_err(""); + printfln_err(" response-time-test [--total-secs n] [--windowed] [--vsync-off]: Run a test to visually inspect the display's response times."); + printfln_err(" width: Width of the rendering resolution to run the test at"); + printfln_err(" height: Height of the rendering resolution to run the test at"); + printfln_err(" refresh_rate: Target refresh rate to run the test at"); + printfln_err(" total-secs: Optional. Total number of seconds to run the test for that count towards the measurement results"); + printfln_err(" windowed: Optional. Run the test in windowed mode (not recommended)"); + printfln_err(" vsync-off: Optional. Run the test with vsync off (not recommended)"); + printfln_err(""); + printfln_err(" vsync-test [--total-secs n] [--windowed]: Run a test to visually inspect the display's vsync behavior."); + printfln_err(" width: Width of the rendering resolution to run the test at"); + printfln_err(" height: Height of the rendering resolution to run the test at"); + printfln_err(" refresh_rate: Target refresh rate to run the test at"); + printfln_err(" total-secs: Optional. Total number of seconds to run the test for that count towards the measurement results"); + printfln_err(" windowed: Optional. Run the test in windowed mode (not recommended)"); +} + +static bool _adapter() +{ + D3DADAPTER_IDENTIFIER9 identifier; + + if (!gfx_adapter_info_get(&identifier)) { + return false; + } + + printfln_out("Driver: %s", identifier.Driver); + printfln_out("Description: %s", identifier.Description); + printfln_out("DeviceName: %s", identifier.DeviceName); +#ifdef _WIN32 + printfln_out("DriverVersion: %lld", identifier.DriverVersion.QuadPart); +#else + printfln_out("DriverVersion: %lu.%lu", identifier.DriverVersionHighPart, identifier.DriverVersionLowPart); +#endif + printfln_out("VendorId: %lu", identifier.VendorId); + printfln_out("DeviceId: %lu", identifier.DeviceId); + printfln_out("SubSysId: %lu", identifier.SubSysId); + printfln_out("Revision: %lu", identifier.Revision); + printfln_out("DeviceIdentifier: {%08lX-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}", + identifier.DeviceIdentifier.Data1, + identifier.DeviceIdentifier.Data2, + identifier.DeviceIdentifier.Data3, + identifier.DeviceIdentifier.Data4[0], + identifier.DeviceIdentifier.Data4[1], + identifier.DeviceIdentifier.Data4[2], + identifier.DeviceIdentifier.Data4[3], + identifier.DeviceIdentifier.Data4[4], + identifier.DeviceIdentifier.Data4[5], + identifier.DeviceIdentifier.Data4[6], + identifier.DeviceIdentifier.Data4[7]); + printfln_out("WHQLLevel: %lu", identifier.WHQLLevel); + + return true; +} + +static bool _modes() +{ + gfx_adapter_modes_t modes; + + if (!gfx_adapter_modes_get(&modes)) { + return false; + } + + for (uint32_t i = 0; i < modes.count; i++) { + printfln_out("%d: %d x %d @ %d hz", i, modes.modes[i].Width, modes.modes[i].Height, modes.modes[i].RefreshRate); + } + + return true; +} + +static bool _refresh_rate_test( + uint32_t width, + uint32_t height, + uint32_t refresh_rate, + uint32_t warm_up_frame_count, + uint32_t sample_frame_count, + uint32_t result_timeout_frame_count, + bool windowed, + bool vsync) +{ + gfx_t *gfx; + input_t *input; + refresh_rate_test_t *test; + gfx_info_t gfx_info; + refresh_rate_test_results_t results; + uint32_t results_timeout_seconds; + + input_init(&input); + + if (!gfx_init(width, height, refresh_rate, windowed, vsync, &gfx)) { + input_fini(input); + return false; + } + + if (!refresh_rate_test_init(gfx, warm_up_frame_count, sample_frame_count, &test)) { + input_fini(input); + gfx_fini(gfx); + return false; + } + + do { + input_update(input); + + if (input_key_esc_pushed(input)) { + break; + } + } while (refresh_rate_test_frame_update(test) && + gfx_last_frame_count_get(gfx) < warm_up_frame_count + sample_frame_count); + + results_timeout_seconds = result_timeout_frame_count / refresh_rate; + + // Display final results + for (uint32_t i = 0; i < result_timeout_frame_count; i++) { + input_update(input); + + if (input_key_esc_pushed(input)) { + break; + } + + if (!refresh_rate_test_results_frame_update(test, results_timeout_seconds)) { + break; + } + } + + gfx_info_get(gfx, &gfx_info); + refresh_rate_test_results_get(test, &results); + + printfln_err("Final results"); + printfln_out("GPU: %s", gfx_info.adapter_identifier); + printfln_out("Spec: %d x %d @ %d hz, %s, vsync %s", gfx_info.width, gfx_info.height, gfx_info.refresh_rate, + gfx_info.windowed ? "windowed" : "fullscreen", gfx_info.vsync ? "on" : "off"); + printfln_out("Total warm-up frame count: %d", results.total_warm_up_frame_count); + printfln_out("Total sample frame count: %d", results.total_sample_frame_count); + printfln_out("Avg frame time (ms): %.3f", results.avg_frame_time_ms); + printfln_out("Avg refresh rate (hz): %.3f", results.avg_refresh_rate_hz); + + refresh_rate_test_fini(test); + input_fini(input); + gfx_fini(gfx); + + return true; +} + +static bool _response_time_test( + uint32_t width, + uint32_t height, + uint32_t refresh_rate, + uint32_t total_frame_count, + bool windowed, + bool vsync) +{ + gfx_t *gfx; + input_t *input; + response_time_test_t *test; + + input_init(&input); + + // Force vsync on, off option doesn't make sense for this test + if (!gfx_init(width, height, refresh_rate, windowed, vsync, &gfx)) { + input_fini(input); + return false; + } + + if (!response_time_test_init(gfx, &test)) { + input_fini(input); + gfx_fini(gfx); + return false; + } + + do { + input_update(input); + + if (input_key_esc_pushed(input)) { + break; + } + } while (response_time_test_frame_update(test) && + gfx_last_frame_count_get(gfx) < total_frame_count); + + response_time_test_fini(test); + input_fini(input); + gfx_fini(gfx); + + return true; +} + +static bool _vsync_test( + uint32_t width, + uint32_t height, + uint32_t refresh_rate, + uint32_t total_frame_count, + bool windowed) +{ + gfx_t *gfx; + input_t *input; + vsync_test_t *test; + + input_init(&input); + + // Force vsync on, off option doesn't make sense for this test + if (!gfx_init(width, height, refresh_rate, windowed, true, &gfx)) { + input_fini(input); + return false; + } + + if (!vsync_test_init(gfx, &test)) { + input_fini(input); + gfx_fini(gfx); + return false; + } + + do { + input_update(input); + + if (input_key_esc_pushed(input)) { + break; + } + } while (vsync_test_frame_update(test) && + gfx_last_frame_count_get(gfx) < total_frame_count); + + vsync_test_fini(test); + input_fini(input); + gfx_fini(gfx); + + return true; +} + +static bool _cmd_refresh_rate_test(int argc, char **argv) +{ + uint32_t width; + uint32_t height; + uint32_t refresh_rate; + uint32_t warm_up_seconds; + uint32_t sample_seconds; + uint32_t results_timeout_seconds; + bool windowed; + bool vsync; + + uint32_t total_warm_up_frame_count; + uint32_t total_sample_frame_count; + uint32_t result_timeout_frame_count; + + if (argc < 3) { + _print_synopsis(); + printfln_err("ERROR: Insufficient arguments"); + return false; + } + + width = atoi(argv[0]); + + if (width == 0 || width > 16384) { + _print_synopsis(); + printfln_err("ERROR: Invalid width: %d", width); + return false; + } + + height = atoi(argv[1]); + + if (height == 0 || height > 16384) { + _print_synopsis(); + printfln_err("ERROR: Invalid height: %d", height); + return false; + } + + refresh_rate = atoi(argv[2]); + + if (refresh_rate == 0 || refresh_rate > 1000) { + _print_synopsis(); + printfln_err("ERROR: Invalid refresh rate: %d", refresh_rate); + return false; + } + + // Sane defaults + warm_up_seconds = 10; + sample_seconds = 20; + results_timeout_seconds = 5; + windowed = false; + vsync = true; + + for (int i = 3; i < argc; i++) { + if (!strcmp(argv[i], "--warm-up-secs")) { + if (i + 1 < argc) { + warm_up_seconds = atoi(argv[++i]); + + if (warm_up_seconds == 0) { + _print_synopsis(); + printfln_err("ERROR: Invalid warm-up seconds: %d", warm_up_seconds); + return false; + } + } else { + _print_synopsis(); + printfln_err("ERROR: Missing argument for --warm-up-secs"); + return false; + } + } else if (!strcmp(argv[i], "--sample-secs")) { + if (i + 1 < argc) { + sample_seconds = atoi(argv[++i]); + + if (sample_seconds == 0) { + _print_synopsis(); + printfln_err("ERROR: Invalid sample seconds: %d", sample_seconds); + return false; + } + } else { + _print_synopsis(); + printfln_err("ERROR: Missing argument for --total-secs"); + return false; + } + } else if (!strcmp(argv[i], "--results-timeout-secs")) { + if (i + 1 < argc) { + results_timeout_seconds = atoi(argv[++i]); + } else { + _print_synopsis(); + printfln_err("ERROR: Missing argument for --results-timeout-secs"); + } + } else if (!strcmp(argv[i], "--windowed")) { + windowed = true; + } else if (!strcmp(argv[i], "--vsync-off")) { + vsync = false; + } + } + + total_warm_up_frame_count = warm_up_seconds * refresh_rate; + total_sample_frame_count = sample_seconds * refresh_rate; + result_timeout_frame_count = results_timeout_seconds * refresh_rate; + + return _refresh_rate_test( + width, + height, + refresh_rate, + total_warm_up_frame_count, + total_sample_frame_count, + result_timeout_frame_count, + windowed, + vsync); +} + +static bool _cmd_response_time_test(int argc, char **argv) +{ + uint32_t width; + uint32_t height; + uint32_t refresh_rate; + uint32_t total_seconds; + bool windowed; + bool vsync; + + uint32_t total_frame_count; + + if (argc < 3) { + _print_synopsis(); + printfln_err("ERROR: Insufficient arguments"); + return false; + } + + width = atoi(argv[0]); + + if (width == 0 || width > 16384) { + _print_synopsis(); + printfln_err("ERROR: Invalid width: %d", width); + return false; + } + + height = atoi(argv[1]); + + if (height == 0 || height > 16384) { + _print_synopsis(); + printfln_err("ERROR: Invalid height: %d", height); + return false; + } + + refresh_rate = atoi(argv[2]); + + if (refresh_rate == 0 || refresh_rate > 1000) { + _print_synopsis(); + printfln_err("ERROR: Invalid refresh rate: %d", refresh_rate); + return false; + } + + // Sane defaults + total_seconds = 30; + windowed = false; + vsync = true; + for (int i = 3; i < argc; i++) { + if (!strcmp(argv[i], "--total-secs")) { + if (i + 1 < argc) { + total_seconds = atoi(argv[++i]); + + if (total_seconds == 0) { + _print_synopsis(); + printfln_err("ERROR: Invalid total seconds: %d", total_seconds); + return false; + } + } else { + _print_synopsis(); + printfln_err("ERROR: Missing argument for --total-secs"); + return false; + } + } else if (!strcmp(argv[i], "--windowed")) { + windowed = true; + } else if (!strcmp(argv[i], "--vsync-off")) { + vsync = false; + } + } + + total_frame_count = total_seconds * refresh_rate; + + return _response_time_test(width, height, refresh_rate, total_frame_count, windowed, vsync); +} + +static bool _cmd_vsync_test(int argc, char **argv) +{ + uint32_t width; + uint32_t height; + uint32_t refresh_rate; + uint32_t total_seconds; + bool windowed; + + uint32_t total_frame_count; + + if (argc < 3) { + _print_synopsis(); + printfln_err("ERROR: Insufficient arguments"); + return false; + } + + width = atoi(argv[0]); + + if (width == 0 || width > 16384) { + _print_synopsis(); + printfln_err("ERROR: Invalid width: %d", width); + return false; + } + + height = atoi(argv[1]); + + if (height == 0 || height > 16384) { + _print_synopsis(); + printfln_err("ERROR: Invalid height: %d", height); + return false; + } + + refresh_rate = atoi(argv[2]); + + if (refresh_rate == 0 || refresh_rate > 1000) { + _print_synopsis(); + printfln_err("ERROR: Invalid refresh rate: %d", refresh_rate); + return false; + } + + // Sane defaults + total_seconds = 30; + windowed = false; + + for (int i = 3; i < argc; i++) { + if (!strcmp(argv[i], "--total-secs")) { + if (i + 1 < argc) { + total_seconds = atoi(argv[++i]); + + if (total_seconds == 0) { + _print_synopsis(); + printfln_err("ERROR: Invalid total seconds: %d", total_seconds); + return false; + } + } else { + _print_synopsis(); + printfln_err("ERROR: Missing argument for --total-secs"); + return false; + } + } else if (!strcmp(argv[i], "--windowed")) { + windowed = true; + } + } + + total_frame_count = total_seconds * refresh_rate; + + return _vsync_test(width, height, refresh_rate, total_frame_count, windowed); +} + +bool cmdline_main(int argc, char **argv) +{ + const char *command; + + if (argc < 1) { + _print_synopsis(); + printfln_err("ERROR: Insufficient arguments"); + return false; + } + + command = argv[0]; + + if (!strcmp(command, "adapter")) { + return _adapter(); + } else if (!strcmp(command, "modes")) { + return _modes(); + } else if (!strcmp(command, "refresh-rate-test")) { + return _cmd_refresh_rate_test(argc - 1, argv + 1); + } else if (!strcmp(command, "response-time-test")) { + return _cmd_response_time_test(argc - 1, argv + 1); + } else if (!strcmp(command, "vsync-test")) { + return _cmd_vsync_test(argc - 1, argv + 1); + } else { + _print_synopsis(argv[0]); + printfln_err("ERROR: Unknown command: %s", command); + return false; + } +} \ No newline at end of file diff --git a/src/main/d3d9-monitor-check/cmdline.h b/src/main/d3d9-monitor-check/cmdline.h new file mode 100644 index 0000000..43303b8 --- /dev/null +++ b/src/main/d3d9-monitor-check/cmdline.h @@ -0,0 +1,8 @@ +#ifndef D3D9_MONITOR_CHECK_CMDLINE_H +#define D3D9_MONITOR_CHECK_CMDLINE_H + +#include + +bool cmdline_main(int argc, char **argv); + +#endif diff --git a/src/main/d3d9-monitor-check/font.c b/src/main/d3d9-monitor-check/font.c new file mode 100644 index 0000000..a281799 --- /dev/null +++ b/src/main/d3d9-monitor-check/font.c @@ -0,0 +1,207 @@ +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include "d3d9-monitor-check/font.h" +#include "d3d9-monitor-check/gfx.h" + +#include "util/mem.h" + +#define BASE_RESOLUTION_WIDTH 640.0f +#define BASE_RESOLUTION_HEIGHT 480.0f +#define BASE_FONT_SIZE 20.0f +#define BASE_MARGIN 1.0f +// Standard Windows DPI +#define BASE_DPI 96.0f + +typedef struct font { + gfx_t *gfx; + ID3DXFont *font; + float scale_x; + float scale_y; + float dpi_scale; + uint32_t font_height; + uint32_t line_height; + uint32_t base_offset_x; + uint32_t base_offset_y; +} font_t; + +static void _font_text_draw(font_t *font, uint32_t x, uint32_t y, uint8_t r, uint8_t g, uint8_t b, const char *text) +{ + uint32_t scaled_x; + uint32_t scaled_y; + RECT rect; + + assert(font); + assert(text); + + // Scale position based on both resolution and DPI + scaled_x = (uint32_t)(x * font->scale_x * font->dpi_scale) + font->base_offset_x; + scaled_y = (uint32_t)(y * font->scale_y * font->dpi_scale) + font->base_offset_y; + + rect.left = scaled_x; + rect.top = scaled_y; + // Scale text box width based on both resolution and DPI + rect.right = scaled_x + (uint32_t)(BASE_RESOLUTION_WIDTH * font->scale_x * font->dpi_scale); + rect.bottom = scaled_y + font->line_height; + + ID3DXFont_DrawText( + font->font, + NULL, + text, + -1, + &rect, + DT_LEFT | DT_TOP | DT_NOCLIP, + D3DCOLOR_XRGB(r, g, b)); +} + +bool font_init(gfx_t *gfx, uint32_t size, font_t **font) +{ + IDirect3DDevice9 *device; + HRESULT hr; + HDC hdc; + int dpi; + + assert(gfx); + assert(font); + + *font = xmalloc(sizeof(font_t)); + memset(*font, 0, sizeof(font_t)); + + (*font)->gfx = gfx; + + // Get the system DPI + hdc = GetDC(NULL); + dpi = GetDeviceCaps(hdc, LOGPIXELSY); + ReleaseDC(NULL, hdc); + + // Calculate DPI scale relative to base DPI + (*font)->dpi_scale = dpi / BASE_DPI; + + // Calculate resolution scaling factors + (*font)->scale_x = gfx_width_get(gfx) / BASE_RESOLUTION_WIDTH; + (*font)->scale_y = gfx_height_get(gfx) / BASE_RESOLUTION_HEIGHT; + + // Scale font height based on both DPI and resolution + // This ensures text maintains physical size across different resolutions + (*font)->font_height = (uint32_t)(size * (*font)->dpi_scale); + + // Line height matches font height + (*font)->line_height = (*font)->font_height; + + // Scale margins based on DPI and resolution + (*font)->base_offset_x = (uint32_t)(BASE_MARGIN * (*font)->scale_x * (*font)->dpi_scale); + (*font)->base_offset_y = (uint32_t)(BASE_MARGIN * (*font)->scale_y * (*font)->dpi_scale); + + device = gfx_device_get(gfx); + + hr = D3DXCreateFont( + device, + (*font)->font_height, + 0, + FW_BOLD, + 1, + FALSE, + DEFAULT_CHARSET, + OUT_DEFAULT_PRECIS, + ANTIALIASED_QUALITY, // Changed to antialiased for better readability + DEFAULT_PITCH | FF_DONTCARE, + "Arial", + &(*font)->font); + + if (hr != D3D_OK) { + free(*font); + return false; + } + + return true; +} + +void font_fini(font_t *font) +{ + assert(font); + + ID3DXFont_Release(font->font); + + free(font); +} + +void font_text_begin(font_t *font, uint32_t line_spacing, uint32_t origin_x, uint32_t origin_y, font_text_t *text) +{ + assert(font); + assert(text); + + text->font = font; + text->line_spacing = line_spacing; + text->origin_x = origin_x; + text->origin_y = origin_y; + text->current_x = origin_x; + text->current_y = origin_y; +} + +void font_text_white_draw(font_text_t *text, const char *fmt, ...) +{ + va_list args; + char buffer[4096]; + + assert(text); + assert(fmt); + + va_start(args, fmt); + vsprintf(buffer, fmt, args); + va_end(args); + + _font_text_draw(text->font, text->current_x, text->current_y, 255, 255, 255, buffer); +} + +void font_text_red_draw(font_text_t *text, const char *fmt, ...) +{ + va_list args; + char buffer[4096]; + + assert(text); + assert(fmt); + + va_start(args, fmt); + vsprintf(buffer, fmt, args); + va_end(args); + + _font_text_draw(text->font, text->current_x, text->current_y, 255, 0, 0, buffer); +} + +void font_text_cyan_draw(font_text_t *text, const char *fmt, ...) +{ + va_list args; + char buffer[4096]; + + assert(text); + assert(fmt); + + va_start(args, fmt); + vsprintf(buffer, fmt, args); + va_end(args); + + _font_text_draw(text->font, text->current_x, text->current_y, 0, 255, 255, buffer); +} + +void font_text_newline(font_text_t *text) +{ + assert(text); + + text->current_x = text->origin_x; + text->current_y += text->font->line_height + text->line_spacing; +} + +void font_text_end(font_text_t *text) +{ + assert(text); + + memset(text, 0, sizeof(font_text_t)); +} \ No newline at end of file diff --git a/src/main/d3d9-monitor-check/font.h b/src/main/d3d9-monitor-check/font.h new file mode 100644 index 0000000..a3be599 --- /dev/null +++ b/src/main/d3d9-monitor-check/font.h @@ -0,0 +1,37 @@ +#ifndef D3D9_MONITOR_CHECK_FONT_H +#define D3D9_MONITOR_CHECK_FONT_H + +#include +#include +#include + +#include "d3d9-monitor-check/gfx.h" + +typedef struct font font_t; + +typedef struct font_text { + font_t *font; + uint32_t line_spacing; + uint32_t origin_x; + uint32_t origin_y; + uint32_t current_x; + uint32_t current_y; +} font_text_t; + +bool font_init(gfx_t *gfx, uint32_t size, font_t **font); + +void font_fini(font_t *font); + +void font_text_begin(font_t *font, uint32_t line_spacing, uint32_t origin_x, uint32_t origin_y, font_text_t *text); + +void font_text_white_draw(font_text_t *text, const char *fmt, ...); + +void font_text_red_draw(font_text_t *text, const char *fmt, ...); + +void font_text_cyan_draw(font_text_t *text, const char *fmt, ...); + +void font_text_newline(font_text_t *text); + +void font_text_end(font_text_t *text); + +#endif diff --git a/src/main/d3d9-monitor-check/gfx.c b/src/main/d3d9-monitor-check/gfx.c new file mode 100644 index 0000000..081301d --- /dev/null +++ b/src/main/d3d9-monitor-check/gfx.c @@ -0,0 +1,430 @@ +#include + +#include +#include + +#include +#include +#include + +#include "d3d9-monitor-check/gfx.h" +#include "d3d9-monitor-check/print-console.h" + +#include "util/mem.h" +#include "util/time.h" + +typedef struct gfx_ctx_t { + HWND hwnd; + IDirect3D9 *d3d; + IDirect3DDevice9 *device; + uint32_t width; + uint32_t height; + uint32_t refresh_rate; + bool windowed; + bool vsync; +} gfx_ctx_t; + +typedef struct gfx_render_state_t { + uint64_t frame_current; + uint64_t frame_start_time; + uint64_t last_frame_time_us; +} gfx_render_state_t; + +typedef struct gfx { + gfx_ctx_t ctx; + gfx_render_state_t render_state; +} gfx_t; + +static const D3DFORMAT _gfx_d3dformat = D3DFMT_X8R8G8B8; + +static bool _gfx_d3d_context_create(IDirect3D9 **d3d) +{ + // Initialize D3D + *d3d = Direct3DCreate9(D3D_SDK_VERSION); + + if (!*d3d) { + printfln_winerr("Creating d3d context failed"); + return false; + } + + return true; +} + +static bool _gfx_adapter_identifier_query(IDirect3D9 *d3d, D3DADAPTER_IDENTIFIER9 *identifier) +{ + HRESULT hr; + + hr = IDirect3D9_GetAdapterIdentifier(d3d, D3DADAPTER_DEFAULT, 0, identifier); + + if (hr != D3D_OK) { + printfln_winerr("GetAdapterIdentifier failed"); + return false; + } + + return true; +} + +static bool _gfx_window_create(uint32_t width, uint32_t height, HWND *hwnd) +{ + WNDCLASSEX wc; + + memset(&wc, 0, sizeof(wc)); + + wc.cbSize = sizeof(wc); + wc.lpfnWndProc = DefWindowProc; + wc.hInstance = GetModuleHandle(NULL); + wc.lpszClassName = "D3D9MonitorCheck"; + + RegisterClassExA(&wc); + + // Create window + *hwnd = CreateWindowA( + wc.lpszClassName, + "D3D9 Monitor Check", + WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, + CW_USEDEFAULT, + width, + height, + NULL, + NULL, + wc.hInstance, + NULL); + + if (!*hwnd) { + printfln_winerr("Failed to create window"); + return false; + } + + return true; +} + +static bool _gfx_d3d_device_create( + HWND hwnd, + IDirect3D9 *d3d, + uint32_t width, + uint32_t height, + uint32_t refresh_rate, + bool windowed, + bool vsync, + IDirect3DDevice9 **device) +{ + D3DPRESENT_PARAMETERS pp; + HRESULT hr; + + memset(&pp, 0, sizeof(pp)); + + if (windowed) { + ShowWindow(hwnd, SW_SHOW); + + pp.Windowed = TRUE; + pp.FullScreen_RefreshRateInHz = 0; + } else { + ShowCursor(FALSE); + + pp.Windowed = FALSE; + pp.FullScreen_RefreshRateInHz = refresh_rate; + } + + if (vsync) { + pp.PresentationInterval = D3DPRESENT_INTERVAL_ONE; + } else { + pp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE; + } + + pp.BackBufferWidth = width; + pp.BackBufferHeight = height; + pp.BackBufferFormat = _gfx_d3dformat; + pp.BackBufferCount = 2; + pp.MultiSampleType = D3DMULTISAMPLE_NONE; + pp.MultiSampleQuality = 0; + pp.SwapEffect = D3DSWAPEFFECT_DISCARD; + pp.hDeviceWindow = hwnd; + pp.EnableAutoDepthStencil = TRUE; + pp.AutoDepthStencilFormat = D3DFMT_D16; + pp.Flags = D3DPRESENTFLAG_LOCKABLE_BACKBUFFER; + + // Create D3D device + hr = IDirect3D9_CreateDevice( + d3d, + D3DADAPTER_DEFAULT, + D3DDEVTYPE_HAL, + hwnd, + D3DCREATE_HARDWARE_VERTEXPROCESSING, + &pp, + device); + + if (hr != D3D_OK) { + printfln_winerr("Creating d3d device failed"); + return false; + } + + return true; +} + +bool gfx_adapter_info_get(D3DADAPTER_IDENTIFIER9 *adapter) +{ + IDirect3D9 *d3d; + + assert(adapter); + + memset(adapter, 0, sizeof(D3DADAPTER_IDENTIFIER9)); + + if (!_gfx_d3d_context_create(&d3d)) { + return false; + } + + if (!_gfx_adapter_identifier_query(d3d, adapter)) { + IDirect3D9_Release(d3d); + return false; + } + + IDirect3D9_Release(d3d); + + return true; +} + +bool gfx_adapter_modes_get(gfx_adapter_modes_t *modes) +{ + IDirect3D9 *d3d; + HRESULT hr; + UINT mode_count; + + assert(modes); + + memset(modes, 0, sizeof(gfx_adapter_modes_t)); + + if (!_gfx_d3d_context_create(&d3d)) { + return false; + } + + mode_count = IDirect3D9_GetAdapterModeCount(d3d, D3DADAPTER_DEFAULT, _gfx_d3dformat); + + if (mode_count > sizeof(modes->modes)) { + mode_count = sizeof(modes->modes); + printfln_err("WARNING: Available adapter modes (total %d) is greater than the maximum supported modes in structure (%zu)", mode_count, sizeof(modes->modes)); + } + + for (UINT i = 0; i < mode_count; i++) { + hr = IDirect3D9_EnumAdapterModes(d3d, D3DADAPTER_DEFAULT, _gfx_d3dformat, i, &modes->modes[i]); + + if (hr != D3D_OK) { + printfln_winerr("EnumAdapterMode index %d failed", i); + IDirect3D9_Release(d3d); + return false; + } + } + + modes->count = mode_count; + + IDirect3D9_Release(d3d); + + return true; +} + +bool gfx_init( + uint32_t width, + uint32_t height, + uint32_t refresh_rate, + bool windowed, + bool vsync, + gfx_t **gfx) +{ + HWND hwnd; + IDirect3D9 *d3d; + D3DADAPTER_IDENTIFIER9 identifier; + IDirect3DDevice9 *device; + + assert(gfx); + + printfln_err("Creating d3d context ..."); + + if (!_gfx_d3d_context_create(&d3d)) { + return false; + } + + printfln_err("Querying adapter identifier ..."); + + if (!_gfx_adapter_identifier_query(d3d, &identifier)) { + IDirect3D9_Release(d3d); + return false; + } + + printfln_err("Adapter:"); + printfln_err("Driver: %s", identifier.Driver); + printfln_err("Description: %s", identifier.Description); + printfln_err("DeviceName: %s", identifier.DeviceName); +#ifdef _WIN32 + printfln_err("DriverVersion: %lld", identifier.DriverVersion.QuadPart); +#else + printfln_err("DriverVersion: %lu.%lu", identifier.DriverVersionHighPart, identifier.DriverVersionLowPart); +#endif + + printfln_err("Creating window with %dx%d ...", width, height); + + if (!_gfx_window_create(width, height, &hwnd)) { + IDirect3D9_Release(d3d); + return false; + } + + printfln_err("Creating d3d device %d x %d @ %d hz %s vsync %s ...", + width, + height, + refresh_rate, + windowed ? "windowed" : "fullscreen", + vsync ? "on" : "off"); + + if (!_gfx_d3d_device_create( + hwnd, + d3d, + width, + height, + refresh_rate, + windowed, + vsync, + &device)) { + IDirect3D9_Release(d3d); + DestroyWindow(hwnd); + return false; + } + + *gfx = xmalloc(sizeof(gfx_t)); + memset(*gfx, 0, sizeof(gfx_t)); + + (*gfx)->ctx.hwnd = hwnd; + (*gfx)->ctx.d3d = d3d; + (*gfx)->ctx.device = device; + (*gfx)->ctx.width = width; + (*gfx)->ctx.height = height; + (*gfx)->ctx.refresh_rate = refresh_rate; + (*gfx)->ctx.windowed = windowed; + (*gfx)->ctx.vsync = vsync; + + (*gfx)->render_state.frame_current = 0; + (*gfx)->render_state.frame_start_time = 0; + (*gfx)->render_state.last_frame_time_us = 0; + + return true; +} + +uint32_t gfx_width_get(gfx_t *gfx) +{ + assert(gfx); + + return gfx->ctx.width; +} + +uint32_t gfx_height_get(gfx_t *gfx) +{ + assert(gfx); + + return gfx->ctx.height; +} + +bool gfx_info_get(gfx_t *gfx, gfx_info_t *info) +{ + D3DADAPTER_IDENTIFIER9 identifier; + + assert(gfx); + assert(info); + + if (!_gfx_adapter_identifier_query(gfx->ctx.d3d, &identifier)) { + return false; + } + + strncpy(info->adapter_identifier, identifier.Description, sizeof(info->adapter_identifier)); + + info->width = gfx->ctx.width; + info->height = gfx->ctx.height; + info->refresh_rate = gfx->ctx.refresh_rate; + info->windowed = gfx->ctx.windowed; + info->vsync = gfx->ctx.vsync; + + return true; +} + +IDirect3DDevice9 *gfx_device_get(gfx_t *gfx) +{ + assert(gfx); + + return gfx->ctx.device; +} + +bool gfx_frame_begin(gfx_t *gfx) +{ + MSG msg; + + assert(gfx); + + // Required to not make windows think we are stuck and not responding + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + if (msg.message == WM_QUIT) { + return false; + } + + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + IDirect3DDevice9_Clear( + gfx->ctx.device, + 0, + NULL, + D3DCLEAR_TARGET, + D3DCOLOR_XRGB(0, 0, 0), + 1.0f, + 0); + IDirect3DDevice9_BeginScene(gfx->ctx.device); + + return true; +} + +void gfx_frame_end(gfx_t *gfx) +{ + uint64_t end_time; + + assert(gfx); + + IDirect3DDevice9_EndScene(gfx->ctx.device); + IDirect3DDevice9_Present(gfx->ctx.device, NULL, NULL, NULL, NULL); + + // End previous frame time measurement after the frame has been presented (which might include waiting for vsync) + // and start a new frame time measurement immediately + // This ensures that anything that comes after ending the frame and still before starting a new frame, e.g. IO + // processing, is included in the total frame time measurement + // However, this can not include the very first frame + if (gfx->render_state.frame_current > 0) { + end_time = time_get_counter(); + gfx->render_state.last_frame_time_us = time_get_elapsed_us(end_time - gfx->render_state.frame_start_time); + gfx->render_state.frame_start_time = end_time; + } + + gfx->render_state.frame_start_time = time_get_counter(); + + gfx->render_state.frame_current++; +} + +uint64_t gfx_last_frame_count_get(gfx_t *gfx) +{ + assert(gfx); + + return gfx->render_state.frame_current; +} + +uint64_t gfx_last_frame_time_us_get(gfx_t *gfx) +{ + assert(gfx); + + return gfx->render_state.last_frame_time_us; +} + +void gfx_fini(gfx_t *gfx) +{ + assert(gfx); + + IDirect3DDevice9_Release(gfx->ctx.device); + IDirect3D9_Release(gfx->ctx.d3d); + DestroyWindow(gfx->ctx.hwnd); + + free(gfx); +} \ No newline at end of file diff --git a/src/main/d3d9-monitor-check/gfx.h b/src/main/d3d9-monitor-check/gfx.h new file mode 100644 index 0000000..790d862 --- /dev/null +++ b/src/main/d3d9-monitor-check/gfx.h @@ -0,0 +1,59 @@ +#ifndef D3D9_MONITOR_CHECK_GFX_H +#define D3D9_MONITOR_CHECK_GFX_H + +#include + +#include + +#include +#include + +typedef struct gfx gfx_t; +struct IDirect3DDevice9; + +typedef struct gfx_adapter_modes { + D3DDISPLAYMODE modes[1024]; + uint32_t count; +} gfx_adapter_modes_t; + +typedef struct gfx_info { + char adapter_identifier[1024]; + uint32_t width; + uint32_t height; + uint32_t refresh_rate; + bool windowed; + bool vsync; +} gfx_info_t; + +bool gfx_adapter_info_get(D3DADAPTER_IDENTIFIER9 *adapter); + +bool gfx_adapter_modes_get(gfx_adapter_modes_t *modes); + +bool gfx_init(uint32_t width, + uint32_t height, + uint32_t refresh_rate, + bool windowed, + bool vsync, + gfx_t **gfx); + +uint32_t gfx_width_get(gfx_t *gfx); + +uint32_t gfx_height_get(gfx_t *gfx); + +bool gfx_info_get(gfx_t *gfx, gfx_info_t *info); + +IDirect3DDevice9 *gfx_device_get(gfx_t *gfx); + +bool gfx_frame_begin(gfx_t *gfx); + +void gfx_frame_end(gfx_t *gfx); + +uint64_t gfx_last_frame_count_get(gfx_t *gfx); + +uint64_t gfx_last_frame_time_us_get(gfx_t *gfx); + +bool gfx_adpater_info_get(gfx_t *gfx); + +void gfx_fini(gfx_t *gfx); + +#endif diff --git a/src/main/d3d9-monitor-check/input.c b/src/main/d3d9-monitor-check/input.c new file mode 100644 index 0000000..bb862db --- /dev/null +++ b/src/main/d3d9-monitor-check/input.c @@ -0,0 +1,90 @@ +#include + +#include +#include +#include + +#include "util/mem.h" + +typedef struct input_key_state { + bool previous_update_state; + bool current_update_state; +} input_key_state_t; + +typedef struct input { + input_key_state_t esc; + input_key_state_t up; + input_key_state_t down; + input_key_state_t left; + input_key_state_t right; + input_key_state_t enter; +} input_t; + +void input_init(input_t **input) +{ + assert(input); + + *input = xmalloc(sizeof(input_t)); + memset(*input, 0, sizeof(input_t)); +} + +void input_update(input_t *input) +{ + assert(input); + + input->esc.previous_update_state = input->esc.current_update_state; + input->esc.current_update_state = GetAsyncKeyState(VK_ESCAPE) & 0x8000; + + input->up.previous_update_state = input->up.current_update_state; + input->up.current_update_state = GetAsyncKeyState(VK_UP) & 0x8000; + + input->down.previous_update_state = input->down.current_update_state; + input->down.current_update_state = GetAsyncKeyState(VK_DOWN) & 0x8000; + + input->left.previous_update_state = input->left.current_update_state; + input->left.current_update_state = GetAsyncKeyState(VK_LEFT) & 0x8000; + + input->right.previous_update_state = input->right.current_update_state; + input->right.current_update_state = GetAsyncKeyState(VK_RIGHT) & 0x8000; + + input->enter.previous_update_state = input->enter.current_update_state; + input->enter.current_update_state = GetAsyncKeyState(VK_RETURN) & 0x8000; + +} + +bool input_key_esc_pushed(input_t *input) +{ + return input->esc.previous_update_state == false && input->esc.current_update_state == true; +} + +bool input_key_up_pushed(input_t *input) +{ + return input->up.previous_update_state == false && input->up.current_update_state == true; +} + +bool input_key_down_pushed(input_t *input) +{ + return input->down.previous_update_state == false && input->down.current_update_state == true; +} + +bool input_key_left_pushed(input_t *input) +{ + return input->left.previous_update_state == false && input->left.current_update_state == true; +} + +bool input_key_right_pushed(input_t *input) +{ + return input->right.previous_update_state == false && input->right.current_update_state == true; +} + +bool input_key_enter_pushed(input_t *input) +{ + return input->enter.previous_update_state == false && input->enter.current_update_state == true; +} + +void input_fini(input_t *input) +{ + assert(input); + + free(input); +} \ No newline at end of file diff --git a/src/main/d3d9-monitor-check/input.h b/src/main/d3d9-monitor-check/input.h new file mode 100644 index 0000000..e0428ce --- /dev/null +++ b/src/main/d3d9-monitor-check/input.h @@ -0,0 +1,26 @@ +#ifndef D3D9_MONITOR_CHECK_INPUT_H +#define D3D9_MONITOR_CHECK_INPUT_H + +#include + +typedef struct input input_t; + +void input_init(input_t **input); + +void input_update(input_t *input); + +bool input_key_esc_pushed(input_t *input); + +bool input_key_up_pushed(input_t *input); + +bool input_key_down_pushed(input_t *input); + +bool input_key_left_pushed(input_t *input); + +bool input_key_right_pushed(input_t *input); + +bool input_key_enter_pushed(input_t *input); + +void input_fini(input_t *input); + +#endif diff --git a/src/main/d3d9-monitor-check/interactive.c b/src/main/d3d9-monitor-check/interactive.c new file mode 100644 index 0000000..6f74431 --- /dev/null +++ b/src/main/d3d9-monitor-check/interactive.c @@ -0,0 +1,196 @@ +#include +#include +#include + +#include "d3d9-monitor-check/gfx.h" +#include "d3d9-monitor-check/input.h" +#include "d3d9-monitor-check/menu.h" +#include "d3d9-monitor-check/print-console.h" +#include "d3d9-monitor-check/refresh-rate-test.h" +#include "d3d9-monitor-check/response-time-test.h" +#include "d3d9-monitor-check/vsync-test.h" + +typedef enum INTERACTIVE_SCREEN { + INTERACTIVE_SCREEN_MENU = 0, + INTERACTIVE_SCREEN_REFRESH_RATE_TEST = 1, + INTERACTIVE_SCREEN_RESPONSE_TIME_TEST = 2, + INTERACTIVE_SCREEN_VSYNC_TEST = 3, +} interactive_screen_t; + +static void _print_synopsis() +{ + printfln_err("D3D9 monitor check tool interactive mode"); + printfln_err(""); + printfln_err("Usage:"); + printfln_err(" d3d9-monitor-check interactive [--windowed] [--vsync-off]"); + printfln_err(""); + printfln_err(" width: Width of the rendering resolution to run the test at"); + printfln_err(" height: Height of the rendering resolution to run the test at"); + printfln_err(" refresh-rate: Target refresh rate to run the test at"); + printfln_err(" windowed: Optional. Run the test in windowed mode (not recommended)"); + printfln_err(" vsync-off: Optional. Run the test with vsync off (not recommended)"); +} + +static bool _interactive( + uint32_t width, + uint32_t height, + uint32_t refresh_rate, + bool windowed, + bool vsync) +{ + gfx_t *gfx; + input_t *input; + bool loop_running; + interactive_screen_t current_screen; + menu_item_t selected_menu_item; + + menu_t *menu; + refresh_rate_test_t *refresh_rate_test; + response_time_test_t *response_time_test; + vsync_test_t *vsync_test; + + input_init(&input); + + if (!gfx_init(width, height, refresh_rate, windowed, vsync, &gfx)) { + input_fini(input); + return false; + } + + if (!menu_init(gfx, &menu)) { + gfx_fini(gfx); + input_fini(input); + return false; + } + + loop_running = true; + current_screen = INTERACTIVE_SCREEN_MENU; + + while (loop_running) { + input_update(input); + + switch (current_screen) { + case INTERACTIVE_SCREEN_MENU: + if (input_key_esc_pushed(input)) { + loop_running = false; + break; + } else if (input_key_up_pushed(input)) { + menu_select_cursor_move_up(menu); + } else if (input_key_down_pushed(input)) { + menu_select_cursor_move_down(menu); + } else if (input_key_enter_pushed(input)) { + selected_menu_item = menu_item_selected_get(menu); + + switch (selected_menu_item) { + case MENU_ITEM_REFRESH_RATE_TEST: + if (!refresh_rate_test_init(gfx, 600, 1200 ,&refresh_rate_test)) { + loop_running = false; + break; + } + + current_screen = INTERACTIVE_SCREEN_REFRESH_RATE_TEST; + break; + + case MENU_ITEM_RESPONSE_TIME_TEST: + if (!response_time_test_init(gfx, &response_time_test)) { + loop_running = false; + break; + } + + current_screen = INTERACTIVE_SCREEN_RESPONSE_TIME_TEST; + break; + + case MENU_ITEM_VSYNC_TEST: + if (!vsync_test_init(gfx, &vsync_test)) { + loop_running = false; + break; + } + + current_screen = INTERACTIVE_SCREEN_VSYNC_TEST; + break; + + case MENU_ITEM_EXIT: + loop_running = false; + break; + + default: + assert(0); + break; + } + } + + if (!menu_frame_update(menu)) { + loop_running = false; + break; + } + + break; + + case INTERACTIVE_SCREEN_REFRESH_RATE_TEST: + if ( input_key_esc_pushed(input) || + !refresh_rate_test_frame_update(refresh_rate_test)) { + current_screen = INTERACTIVE_SCREEN_MENU; + refresh_rate_test_fini(refresh_rate_test); + } + + break; + + case INTERACTIVE_SCREEN_RESPONSE_TIME_TEST: + if ( input_key_esc_pushed(input) || + !response_time_test_frame_update(response_time_test)) { + current_screen = INTERACTIVE_SCREEN_MENU; + response_time_test_fini(response_time_test); + } + + break; + + case INTERACTIVE_SCREEN_VSYNC_TEST: + if ( input_key_esc_pushed(input) || + !vsync_test_frame_update(vsync_test)) { + current_screen = INTERACTIVE_SCREEN_MENU; + vsync_test_fini(vsync_test); + } + + break; + } + } + + menu_fini(menu); + + gfx_fini(gfx); + input_fini(input); + + return true; +} + +bool interactive_main(int argc, char **argv) +{ + uint32_t width; + uint32_t height; + uint32_t refresh_rate; + bool windowed; + bool vsync; + + assert(argv); + + if (argc < 3) { + _print_synopsis(); + printfln_err("ERROR: Insufficient arguments"); + return false; + } + + width = atoi(argv[0]); + height = atoi(argv[1]); + refresh_rate = atoi(argv[2]); + windowed = false; + vsync = true; + + for (int i = 3; i < argc; i++) { + if (!strcmp(argv[i], "--windowed")) { + windowed = true; + } else if (!strcmp(argv[i], "--vsync-off")) { + vsync = false; + } + } + + return _interactive(width, height, refresh_rate, windowed, vsync); +} \ No newline at end of file diff --git a/src/main/d3d9-monitor-check/interactive.h b/src/main/d3d9-monitor-check/interactive.h new file mode 100644 index 0000000..48032b9 --- /dev/null +++ b/src/main/d3d9-monitor-check/interactive.h @@ -0,0 +1,8 @@ +#ifndef D3D9_MONITOR_CHECK_INTERACTIVE_H +#define D3D9_MONITOR_CHECK_INTERACTIVE_H + +#include + +bool interactive_main(int argc, char **argv); + +#endif diff --git a/src/main/d3d9-monitor-check/main.c b/src/main/d3d9-monitor-check/main.c new file mode 100644 index 0000000..d1b6010 --- /dev/null +++ b/src/main/d3d9-monitor-check/main.c @@ -0,0 +1,41 @@ +#include + +#include "d3d9-monitor-check/cmdline.h" +#include "d3d9-monitor-check/interactive.h" +#include "d3d9-monitor-check/print-console.h" + +static void _print_synopsis() +{ + printfln_err("D3D9 monitor check tool"); + printfln_err(""); + printfln_err("A versatile tool for running various (music game relevant) monitor tests using the D3D9 rendering API"); + printfln_err(""); + printfln_err("Available commands:"); + printfln_err(" cmdline: Run the tool in command line mode"); + printfln_err(" interactive: Run the tool in interactive mode"); +} + +int main(int argc, char **argv) +{ + const char *command; + bool success; + + if (argc < 2) { + _print_synopsis(); + printfln_err("ERROR: Insufficient arguments"); + return 1; + } + + command = argv[1]; + + if (!strcmp(command, "cmdline")) { + success = cmdline_main(argc - 2, argv + 2); + } else if (!strcmp(command, "interactive")) { + success = interactive_main(argc - 2, argv + 2); + } else { + _print_synopsis(); + success = false; + } + + return success ? 0 : 1; +} diff --git a/src/main/d3d9-monitor-check/menu.c b/src/main/d3d9-monitor-check/menu.c new file mode 100644 index 0000000..0c940af --- /dev/null +++ b/src/main/d3d9-monitor-check/menu.c @@ -0,0 +1,114 @@ +#include + +#include "d3d9-monitor-check/gfx.h" +#include "d3d9-monitor-check/font.h" +#include "d3d9-monitor-check/menu.h" + +#include "util/mem.h" + +typedef struct menu { + gfx_t *gfx; + font_t *font; + menu_item_t current_selected_item; +} menu_t; + +bool menu_init( + gfx_t *gfx, + menu_t **menu) +{ + assert(gfx); + assert(menu); + + *menu = xmalloc(sizeof(menu_t)); + (*menu)->gfx = gfx; + (*menu)->current_selected_item = MENU_ITEM_REFRESH_RATE_TEST; + + if (!font_init(gfx, 20, &(*menu)->font)) { + free(*menu); + return false; + } + + return true; +} + +void menu_select_cursor_move_up(menu_t *menu) +{ + assert(menu); + + if (menu->current_selected_item == MENU_ITEM_REFRESH_RATE_TEST) { + menu->current_selected_item = MENU_ITEM_EXIT; + } else { + menu->current_selected_item--; + } +} + +void menu_select_cursor_move_down(menu_t *menu) +{ + assert(menu); + + if (menu->current_selected_item == MENU_ITEM_EXIT) { + menu->current_selected_item = MENU_ITEM_REFRESH_RATE_TEST; + } else { + menu->current_selected_item++; + } +} + +menu_item_t menu_item_selected_get(menu_t *menu) +{ + assert(menu); + + return menu->current_selected_item; +} + +bool menu_frame_update(menu_t *menu) +{ + font_text_t text; + + assert(menu); + + if (!gfx_frame_begin(menu->gfx)) { + return false; + } + + font_text_begin(menu->font, 10, 10, 10, &text); + font_text_white_draw(&text, "D3D9 Monitor Check - Main Menu"); + font_text_newline(&text); + font_text_newline(&text); + if (menu->current_selected_item == MENU_ITEM_REFRESH_RATE_TEST) { + font_text_white_draw(&text, "-> 1. Refresh Rate Test"); + } else { + font_text_white_draw(&text, " 1. Refresh Rate Test"); + } + font_text_newline(&text); + if (menu->current_selected_item == MENU_ITEM_RESPONSE_TIME_TEST) { + font_text_white_draw(&text, "-> 2. Response Time Test"); + } else { + font_text_white_draw(&text, " 2. Response Time Test"); + } + font_text_newline(&text); + if (menu->current_selected_item == MENU_ITEM_VSYNC_TEST) { + font_text_white_draw(&text, "-> 3. VSync Test"); + } else { + font_text_white_draw(&text, " 3. VSync Test"); + } + font_text_newline(&text); + if (menu->current_selected_item == MENU_ITEM_EXIT) { + font_text_white_draw(&text, "-> 4. Exit"); + } else { + font_text_white_draw(&text, " 4. Exit"); + } + font_text_end(&text); + + gfx_frame_end(menu->gfx); + + return true; +} + +void menu_fini(menu_t *menu) +{ + assert(menu); + + font_fini(menu->font); + + free(menu); +} \ No newline at end of file diff --git a/src/main/d3d9-monitor-check/menu.h b/src/main/d3d9-monitor-check/menu.h new file mode 100644 index 0000000..e8abddb --- /dev/null +++ b/src/main/d3d9-monitor-check/menu.h @@ -0,0 +1,32 @@ +#ifndef D3D9_MONITOR_CHECK_MENU_H +#define D3D9_MONITOR_CHECK_MENU_H + +#include +#include + +#include "d3d9-monitor-check/gfx.h" + +typedef enum MENU_ITEM { + MENU_ITEM_REFRESH_RATE_TEST = 0, + MENU_ITEM_RESPONSE_TIME_TEST = 1, + MENU_ITEM_VSYNC_TEST = 2, + MENU_ITEM_EXIT = 3, +} menu_item_t; + +typedef struct menu menu_t; + +bool menu_init( + gfx_t *gfx, + menu_t **menu); + +void menu_select_cursor_move_up(menu_t *menu); + +void menu_select_cursor_move_down(menu_t *menu); + +menu_item_t menu_item_selected_get(menu_t *menu); + +bool menu_frame_update(menu_t *menu); + +void menu_fini(menu_t *menu); + +#endif \ No newline at end of file diff --git a/src/main/d3d9-monitor-check/print-console.h b/src/main/d3d9-monitor-check/print-console.h new file mode 100644 index 0000000..b0e38b8 --- /dev/null +++ b/src/main/d3d9-monitor-check/print-console.h @@ -0,0 +1,22 @@ +#ifndef PRINT_CONSOLE_H +#define PRINT_CONSOLE_H + +#include + +#include "util/winerr.h" + +#define printf_out(fmt, ...) \ + fprintf(stdout, fmt, ##__VA_ARGS__) +#define printf_err(fmt, ...) \ + fprintf(stderr, fmt, ##__VA_ARGS__) +#define printfln_out(fmt, ...) \ + fprintf(stdout, fmt "\n", ##__VA_ARGS__) +#define printfln_err(fmt, ...) \ + fprintf(stderr, fmt "\n", ##__VA_ARGS__) + +#define printfln_winerr(fmt, ...) \ + char *winerr = util_winerr_format_last_error_code(); \ + fprintf(stderr, fmt ": %s\n", ##__VA_ARGS__, winerr); \ + free(winerr); + +#endif diff --git a/src/main/d3d9-monitor-check/refresh-rate-test.c b/src/main/d3d9-monitor-check/refresh-rate-test.c new file mode 100644 index 0000000..d872514 --- /dev/null +++ b/src/main/d3d9-monitor-check/refresh-rate-test.c @@ -0,0 +1,304 @@ +#include +#include +#include +#include + +#include "d3d9-monitor-check/gfx.h" +#include "d3d9-monitor-check/font.h" +#include "d3d9-monitor-check/refresh-rate-test.h" + +#include "util/mem.h" + +typedef enum refresh_rate_test_phase { + REFRESH_RATE_TEST_PHASE_WARM_UP = 0, + REFRESH_RATE_TEST_PHASE_MEASURE = 1, + REFRESH_RATE_TEST_PHASE_DONE = 2 +} refresh_rate_test_phase_t; + +typedef struct refresh_rate_test_ctx { + gfx_t *gfx; + font_t *font; + uint32_t total_warm_up_frame_count; + uint32_t total_sample_frame_count; +} refresh_rate_test_ctx_t; + +typedef struct refresh_rate_test_state { + gfx_info_t gfx_info; + refresh_rate_test_phase_t phase; + uint64_t last_frame_time_us; + uint32_t warm_up_frame_count; + uint32_t sample_frame_count; + uint64_t warm_up_elapsed_us; + uint64_t sample_elapsed_us; +} refresh_rate_test_state_t; + +typedef struct refresh_rate_test { + refresh_rate_test_ctx_t ctx; + refresh_rate_test_state_t state; +} refresh_rate_test_t; + +static refresh_rate_test_phase_t _refresh_rate_test_phase_evaluate(const refresh_rate_test_t *test) +{ + switch (test->state.phase) { + case REFRESH_RATE_TEST_PHASE_WARM_UP: + if (test->state.warm_up_frame_count < test->ctx.total_warm_up_frame_count) { + return REFRESH_RATE_TEST_PHASE_WARM_UP; + } else { + return REFRESH_RATE_TEST_PHASE_MEASURE; + } + + case REFRESH_RATE_TEST_PHASE_MEASURE: + if (test->state.sample_frame_count < test->ctx.total_sample_frame_count) { + return REFRESH_RATE_TEST_PHASE_MEASURE; + } else { + return REFRESH_RATE_TEST_PHASE_DONE; + } + + case REFRESH_RATE_TEST_PHASE_DONE: + return REFRESH_RATE_TEST_PHASE_DONE; + + default: + assert(0); + return REFRESH_RATE_TEST_PHASE_DONE; + } +} + +bool refresh_rate_test_init( + gfx_t *gfx, + uint32_t total_warm_up_frame_count, + uint32_t total_sample_frame_count, + refresh_rate_test_t **test) +{ + assert(gfx); + assert(test); + + *test = xmalloc(sizeof(refresh_rate_test_t)); + memset(*test, 0, sizeof(refresh_rate_test_t)); + + if (!font_init(gfx, 20, &(*test)->ctx.font)) { + free(*test); + return false; + } + + font_init(gfx, 20, &(*test)->ctx.font); + + (*test)->ctx.gfx = gfx; + (*test)->ctx.total_warm_up_frame_count = total_warm_up_frame_count; + (*test)->ctx.total_sample_frame_count = total_sample_frame_count; + + (*test)->state.phase = REFRESH_RATE_TEST_PHASE_WARM_UP; + (*test)->state.last_frame_time_us = 0; + (*test)->state.warm_up_frame_count = 0; + (*test)->state.sample_frame_count = 0; + (*test)->state.warm_up_elapsed_us = 0; + (*test)->state.sample_elapsed_us = 0; + + gfx_info_get(gfx, &(*test)->state.gfx_info); + + return true; +} + +static bool _refresh_rate_test_phase_warm_up_frame_update(refresh_rate_test_t *test) +{ + font_text_t text; + + assert(test); + + if (!gfx_frame_begin(test->ctx.gfx)) { + return false; + } + + font_text_begin(test->ctx.font, 0, 10, 10, &text); + + font_text_white_draw(&text, "Refresh Rate Test"); + font_text_newline(&text); + font_text_white_draw(&text, "GPU: %s", test->state.gfx_info.adapter_identifier); + font_text_newline(&text); + font_text_white_draw(&text, "Spec: %d x %d @ %d hz, %s, vsync %s", test->state.gfx_info.width, test->state.gfx_info.height, test->state.gfx_info.refresh_rate, + test->state.gfx_info.windowed ? "windowed" : "fullscreen", test->state.gfx_info.vsync ? "on" : "off"); + font_text_newline(&text); + font_text_newline(&text); + + // First frame won't have any data available causing division by zero in the stats + if (test->state.warm_up_frame_count != 0) { + font_text_white_draw(&text, "Status: Warm-up in progress ..."); + font_text_newline(&text); + + font_text_white_draw(&text, "Frame: %d / %d", test->state.warm_up_frame_count, test->ctx.total_warm_up_frame_count); + font_text_newline(&text); + font_text_white_draw(&text, "Last frame time: %.3f ms", test->state.last_frame_time_us / 1000.0f); + font_text_newline(&text); + font_text_white_draw(&text, "Avg frame time: %.3f ms", test->state.warm_up_elapsed_us / test->state.warm_up_frame_count / 1000.0f); + font_text_newline(&text); + font_text_white_draw(&text, "Last refresh rate: %.3f Hz", 1000.0f / (test->state.last_frame_time_us / 1000.0f)); + font_text_newline(&text); + font_text_white_draw(&text, "Avg refresh rate: %.3f Hz", 1000.0f / (test->state.warm_up_elapsed_us / test->state.warm_up_frame_count / 1000.0f)); + font_text_newline(&text); + } + + font_text_newline(&text); + font_text_white_draw(&text, "Press ESC to exit early"); + + font_text_end(&text); + + gfx_frame_end(test->ctx.gfx); + + test->state.last_frame_time_us = gfx_last_frame_time_us_get(test->ctx.gfx); + test->state.warm_up_elapsed_us += test->state.last_frame_time_us; + test->state.warm_up_frame_count++; + + return true; +} + +static bool _refresh_rate_test_phase_measure_frame_update(refresh_rate_test_t *test) +{ + font_text_t text; + + assert(test); + + if (!gfx_frame_begin(test->ctx.gfx)) { + return false; + } + + font_text_begin(test->ctx.font, 0, 10, 10, &text); + + font_text_white_draw(&text, "Refresh Rate Test"); + font_text_newline(&text); + font_text_white_draw(&text, "GPU: %s", test->state.gfx_info.adapter_identifier); + font_text_newline(&text); + font_text_white_draw(&text, "Spec: %d x %d @ %d hz, %s, vsync %s", test->state.gfx_info.width, test->state.gfx_info.height, test->state.gfx_info.refresh_rate, + test->state.gfx_info.windowed ? "windowed" : "fullscreen", test->state.gfx_info.vsync ? "on" : "off"); + font_text_newline(&text); + font_text_newline(&text); + + // First frame won't have any data available causing division by zero in the stats + if (test->state.sample_frame_count != 0) { + font_text_white_draw(&text, "Status: Measuring in progress ..."); + font_text_newline(&text); + + font_text_white_draw(&text, "Frame: %d / %d", test->state.sample_frame_count, test->ctx.total_sample_frame_count); + font_text_newline(&text); + font_text_white_draw(&text, "Last frame time: %.3f ms", test->state.last_frame_time_us / 1000.0f); + font_text_newline(&text); + font_text_white_draw(&text, "Avg frame time: %.3f ms", test->state.sample_elapsed_us / test->state.sample_frame_count / 1000.0f); + font_text_newline(&text); + font_text_white_draw(&text, "Last refresh rate: %.3f Hz", 1000.0f / (test->state.last_frame_time_us / 1000.0f)); + font_text_newline(&text); + font_text_white_draw(&text, "Avg refresh rate: %.3f Hz", 1000.0f / (test->state.sample_elapsed_us / test->state.sample_frame_count / 1000.0f)); + font_text_newline(&text); + } + + font_text_newline(&text); + font_text_white_draw(&text, "Press ESC to exit early"); + + font_text_end(&text); + + gfx_frame_end(test->ctx.gfx); + + test->state.last_frame_time_us = gfx_last_frame_time_us_get(test->ctx.gfx); + test->state.sample_elapsed_us += test->state.last_frame_time_us; + test->state.sample_frame_count++; + + return true; +} + +bool refresh_rate_test_frame_update(refresh_rate_test_t *test) +{ + assert(test); + + test->state.phase = _refresh_rate_test_phase_evaluate(test); + + switch (test->state.phase) { + case REFRESH_RATE_TEST_PHASE_WARM_UP: + return _refresh_rate_test_phase_warm_up_frame_update(test); + + case REFRESH_RATE_TEST_PHASE_MEASURE: + return _refresh_rate_test_phase_measure_frame_update(test); + + case REFRESH_RATE_TEST_PHASE_DONE: + return false; + + default: + assert(0); + return false; + } +} + +void refresh_rate_test_results_get(const refresh_rate_test_t *test, refresh_rate_test_results_t *results) +{ + assert(test); + assert(results); + + results->total_warm_up_frame_count = test->state.warm_up_frame_count; + results->total_sample_frame_count = test->state.sample_frame_count; + + if (test->state.sample_elapsed_us > 0 && test->state.sample_frame_count > 0) { + results->avg_frame_time_ms = test->state.sample_elapsed_us / test->state.sample_frame_count / 1000.0f; + results->avg_refresh_rate_hz = 1000.0f / results->avg_frame_time_ms; + } else { + results->avg_frame_time_ms = 0; + results->avg_refresh_rate_hz = 0; + } +} + +bool refresh_rate_test_results_frame_update(refresh_rate_test_t *test, uint32_t results_timeout_seconds) +{ + refresh_rate_test_results_t results; + font_text_t text; + + assert(test); + + if (!gfx_frame_begin(test->ctx.gfx)) { + return false; + } + + refresh_rate_test_results_get(test, &results); + + font_text_begin(test->ctx.font, 0, 10, 10, &text); + + font_text_white_draw(&text, "Refresh Rate Test"); + font_text_newline(&text); + font_text_white_draw(&text, "GPU: %s", test->state.gfx_info.adapter_identifier); + font_text_newline(&text); + font_text_white_draw(&text, "Spec: %d x %d @ %d hz, %s, vsync %s", test->state.gfx_info.width, test->state.gfx_info.height, test->state.gfx_info.refresh_rate, + test->state.gfx_info.windowed ? "windowed" : "fullscreen", test->state.gfx_info.vsync ? "on" : "off"); + font_text_newline(&text); + font_text_newline(&text); + + if (test->ctx.total_warm_up_frame_count != test->state.warm_up_frame_count || + test->ctx.total_sample_frame_count != test->state.sample_frame_count) { + font_text_white_draw(&text, "Status: Warning, exited early"); + } else { + font_text_white_draw(&text, "Status: Completed"); + } + + font_text_newline(&text); + font_text_white_draw(&text, "Total warm-up frame count: %d", results.total_warm_up_frame_count); + font_text_newline(&text); + font_text_white_draw(&text, "Total sample frame count: %d", results.total_sample_frame_count); + font_text_newline(&text); + font_text_white_draw(&text, "Avg frame time: %.3f ms", results.avg_frame_time_ms); + font_text_newline(&text); + font_text_white_draw(&text, "Avg refresh rate: %.3f Hz", results.avg_refresh_rate_hz); + font_text_newline(&text); + font_text_newline(&text); + + font_text_white_draw(&text, "Exiting in %d seconds ...", results_timeout_seconds); + font_text_newline(&text); + font_text_white_draw(&text, "Press ESC to exit immediately"); + + font_text_end(&text); + + gfx_frame_end(test->ctx.gfx); + + return true; +} + +void refresh_rate_test_fini(refresh_rate_test_t *test) +{ + assert(test); + + font_fini(test->ctx.font); + + free(test); +} \ No newline at end of file diff --git a/src/main/d3d9-monitor-check/refresh-rate-test.h b/src/main/d3d9-monitor-check/refresh-rate-test.h new file mode 100644 index 0000000..31969b8 --- /dev/null +++ b/src/main/d3d9-monitor-check/refresh-rate-test.h @@ -0,0 +1,32 @@ +#ifndef D3D9_MONITOR_CHECK_REFRESH_RATE_TEST_H +#define D3D9_MONITOR_CHECK_REFRESH_RATE_TEST_H + +#include +#include + +#include "d3d9-monitor-check/gfx.h" + +typedef struct refresh_rate_test_results_t { + uint32_t total_warm_up_frame_count; + uint32_t total_sample_frame_count; + double avg_frame_time_ms; + double avg_refresh_rate_hz; +} refresh_rate_test_results_t; + +typedef struct refresh_rate_test refresh_rate_test_t; + +bool refresh_rate_test_init( + gfx_t *gfx, + uint32_t total_warm_up_frame_count, + uint32_t total_sample_frame_count, + refresh_rate_test_t **test); + +bool refresh_rate_test_frame_update(refresh_rate_test_t *test); + +void refresh_rate_test_results_get(const refresh_rate_test_t *test, refresh_rate_test_results_t *results); + +bool refresh_rate_test_results_frame_update(refresh_rate_test_t *test, uint32_t results_timeout_seconds); + +void refresh_rate_test_fini(refresh_rate_test_t *test); + +#endif diff --git a/src/main/d3d9-monitor-check/response-time-test.c b/src/main/d3d9-monitor-check/response-time-test.c new file mode 100644 index 0000000..71333c7 --- /dev/null +++ b/src/main/d3d9-monitor-check/response-time-test.c @@ -0,0 +1,123 @@ +#include + +#include + +#include +#include +#include + +#include "d3d9-monitor-check/gfx.h" + +#include "util/mem.h" +#include "util/time.h" + +#define DEFAULT_SCROLL_SPEED_PX_PER_SEC 200.0f +#define DEFAULT_BLOCK_DISTANCE_PX 50 +#define RECT_WIDTH_PX 80 +#define RECT_HEIGHT_PX 20 + +typedef struct response_time_test { + gfx_t *gfx; + float scroll_speed_px_per_sec; + uint32_t block_distance_px; + float current_y_pos; +} response_time_test_t; + +bool response_time_test_init( + gfx_t *gfx, + response_time_test_t **test) +{ + assert(gfx); + assert(test); + + *test = xmalloc(sizeof(response_time_test_t)); + + (*test)->gfx = gfx; + (*test)->scroll_speed_px_per_sec = DEFAULT_SCROLL_SPEED_PX_PER_SEC; + (*test)->block_distance_px = DEFAULT_BLOCK_DISTANCE_PX; + (*test)->current_y_pos = 0; + + return true; +} + +bool response_time_test_frame_update(response_time_test_t *test) +{ + IDirect3DDevice9 *device; + uint32_t screen_height; + uint32_t screen_width; + float delta_time; + D3DRECT rect_white; + D3DRECT rect_blue; + + assert(test); + + device = gfx_device_get(test->gfx); + screen_width = gfx_width_get(test->gfx); + screen_height = gfx_height_get(test->gfx); + + delta_time = gfx_last_frame_time_us_get(test->gfx) / 1000000.0f; + + // Update position + test->current_y_pos += test->scroll_speed_px_per_sec * delta_time; + + // Loop position when rectangles are off screen + if (test->current_y_pos > screen_height + RECT_HEIGHT_PX) { + test->current_y_pos = -RECT_HEIGHT_PX; + } + + // Calculate rectangle positions + rect_white.x1 = (screen_width / 2) - RECT_WIDTH_PX - 10; + rect_white.x2 = rect_white.x1 + RECT_WIDTH_PX; + rect_white.y1 = (int) test->current_y_pos; + rect_white.y2 = rect_white.y1 + RECT_HEIGHT_PX; + + rect_blue.x1 = (screen_width / 2) + 10; + rect_blue.x2 = rect_blue.x1 + RECT_WIDTH_PX; + rect_blue.y1 = (int) test->current_y_pos + test->block_distance_px; + rect_blue.y2 = rect_blue.y1 + RECT_HEIGHT_PX; + + if (!gfx_frame_begin(test->gfx)) { + return false; + } + + // Background + IDirect3DDevice9_Clear( + device, + 0, + NULL, + D3DCLEAR_TARGET, + D3DCOLOR_XRGB(128, 128, 128), + 1.0f, + 0); + + // White rectangle + IDirect3DDevice9_Clear( + device, + 1, + &rect_white, + D3DCLEAR_TARGET, + D3DCOLOR_XRGB(255, 255, 255), + 1.0f, + 0); + + // Blue rectangle + IDirect3DDevice9_Clear( + device, + 1, + &rect_blue, + D3DCLEAR_TARGET, + D3DCOLOR_XRGB(0, 0, 255), + 1.0f, + 0); + + gfx_frame_end(test->gfx); + + return true; +} + +void response_time_test_fini(response_time_test_t *test) +{ + assert(test); + + free(test); +} diff --git a/src/main/d3d9-monitor-check/response-time-test.h b/src/main/d3d9-monitor-check/response-time-test.h new file mode 100644 index 0000000..e428b8c --- /dev/null +++ b/src/main/d3d9-monitor-check/response-time-test.h @@ -0,0 +1,19 @@ +#ifndef D3D9_MONITOR_CHECK_RESPONSE_TIME_TEST_H +#define D3D9_MONITOR_CHECK_RESPONSE_TIME_TEST_H + +#include +#include + +#include "d3d9-monitor-check/gfx.h" + +typedef struct response_time_test response_time_test_t; + +bool response_time_test_init( + gfx_t *gfx, + response_time_test_t **test); + +bool response_time_test_frame_update(response_time_test_t *test); + +void response_time_test_fini(response_time_test_t *test); + +#endif // D3D9_MONITOR_CHECK_RESPONSE_TIME_TEST_H diff --git a/src/main/d3d9-monitor-check/vsync-test.c b/src/main/d3d9-monitor-check/vsync-test.c new file mode 100644 index 0000000..2fdc8f0 --- /dev/null +++ b/src/main/d3d9-monitor-check/vsync-test.c @@ -0,0 +1,69 @@ +#include +#include +#include + +#include "d3d9-monitor-check/gfx.h" +#include "d3d9-monitor-check/font.h" + +#include "util/mem.h" + +typedef struct vsync_test { + gfx_t *gfx; + font_t *font; +} vsync_test_t; + +bool vsync_test_init( + gfx_t *gfx, + vsync_test_t **test) +{ + assert(gfx); + assert(test); + + *test = xmalloc(sizeof(vsync_test_t)); + memset(*test, 0, sizeof(vsync_test_t)); + + if (!font_init(gfx, 160, &(*test)->font)) { + free(*test); + return false; + } + + (*test)->gfx = gfx; + + return true; +} + +bool vsync_test_frame_update(vsync_test_t *test) +{ + uint64_t frame_count; + font_text_t text; + + assert(test); + + frame_count = gfx_last_frame_count_get(test->gfx); + + if (!gfx_frame_begin(test->gfx)) { + return false; + } + + font_text_begin(test->font, 0, 80, 80, &text); + + if (frame_count % 2 == 0) { + font_text_red_draw(&text, "VSYNC"); + } else { + font_text_cyan_draw(&text, "VSYNC"); + } + + font_text_end(&text); + + gfx_frame_end(test->gfx); + + return true; +} + +void vsync_test_fini(vsync_test_t *test) +{ + assert(test); + + font_fini(test->font); + free(test); +} \ No newline at end of file diff --git a/src/main/d3d9-monitor-check/vsync-test.h b/src/main/d3d9-monitor-check/vsync-test.h new file mode 100644 index 0000000..ad6fd97 --- /dev/null +++ b/src/main/d3d9-monitor-check/vsync-test.h @@ -0,0 +1,19 @@ +#ifndef D3D9_MONITOR_CHECK_VSYNC_TEST_H +#define D3D9_MONITOR_CHECK_VSYNC_TEST_H + +#include +#include + +#include "d3d9-monitor-check/gfx.h" + +typedef struct vsync_test vsync_test_t; + +bool vsync_test_init( + gfx_t *gfx, + vsync_test_t **test); + +bool vsync_test_frame_update(vsync_test_t *test); + +void vsync_test_fini(vsync_test_t *test); + +#endif // D3D9_MONITOR_CHECK_VSYNC_TEST_H