Files
spice2x.github.io/src/spice2x/api/serial.cpp
T
bicarus 71ba9b6b47 os: implement win10 high-resolution timer as replacement for Sleep() / sleep_for() (#682)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #681 

## Description of change
`Sleep` and `sleep_for()` can be very inaccurate and varies depending on
what the OS gives us...

### `timeBeginPeriod(1)`
On boot, we are now calling `timeBeginPeriod(1)`, which affects the
whole process but makes `Sleep` more accurate. There is some risk here
if any game was relying on doing things like `Sleep(1)` and expecting it
to run for 15.6ms. Most games already call `timeBeginPeriod(1)` in the
game engine, though not the whole time, so I'm hoping that this is not
too impactful.

### Opt out of Win11 power throttling
Ensure that timer resolution change above is respected even when the
window is occluded / minimized by opting out of throttling via
`PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION`.

### Use Win10 high resolution timer instead of Sleep

On Win10 1803 and above, there is a new OS-level API for high resolution
timers; if this is available, use it
(`CREATE_WAITABLE_TIMER_HIGH_RESOLUTION`). Worth noting that WINE
doesn't support this currently.

If not, fall back to `Sleep`, which is significantly better than
`sleep_for()` in my experiments.

Callers of Sleep / sleep_for were replaced with this new timer. Most of
them anyway; calls to Sleep() with more than 100ms+ was left alone.

### Add an option as a chicken bit

To opt out I'm adding a new option called `Use Legacy Timers` which will
revert to behavior before this PR. The code paths that switched from
`sleep_for` to `Sleep` will remain in place though, not affected by the
option.

## Expected changes
In some I/O emulation modules, poll threads may run more frequently,
resulting in lower latency.

It also means that spice overall may use more CPU resources and power.
If you don't like this, you can always enable the option to opt out;
e.g., if you're on old arcade cab PC.

## Testing

DDR p4io - ok
drs touch hook - ok
IIDX camera hook - ok
CCJ trackball - ok
2026-05-08 02:25:20 -07:00

243 lines
9.3 KiB
C++

#include "controller.h"
#include "serial.h"
#include <string>
#include <utility>
#include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h"
namespace api {
SerialController::SerialController(Controller *controller, std::string port, DWORD baud)
: controller(controller), port(std::move(port)), baud(baud) {
this->state = new ClientState();
controller->init_state(this->state);
this->thread = new std::thread([this] () {
log_warning("api::serial", "listening on {} (baud: {})", this->port, this->baud);
timeutils::PreciseSleepTimer timer;
// read buffer
uint8_t read_buffer[16*1024];
size_t read_buffer_len = 0;
// serial retry loop
while (this->running) {
// try to open port
if (this->handle == INVALID_HANDLE_VALUE) {
this->open_port();
}
// reset in-buffer
read_buffer_len = 0;
// connection loop
DWORD retry_time = 1000;
while (this->handle != INVALID_HANDLE_VALUE) {
retry_time = 1000;
DWORD bytes_read = 0;
// check if we need to wait for incoming data first
bool messages_in_buffer = false;
for (size_t i = 0; i < read_buffer_len; i++) {
if (read_buffer[i] == 0x00) {
messages_in_buffer = true;
break;
}
}
// read data
if (!messages_in_buffer && (!ReadFile(
this->handle,
&read_buffer[read_buffer_len],
sizeof(read_buffer) - read_buffer_len, &bytes_read,
nullptr) || bytes_read == 0)) {
// open new connection
log_warning("api::serial", "read error on {}", this->port);
this->free_port();
break;
} else {
//log_info("api::serial::in", "{}", bin2hex(read_buffer + read_buffer_len, bytes_read));
// check for reset
if (read_buffer_len + bytes_read > 7) {
size_t zero_counter = 0;
for (size_t i = 0; i < read_buffer_len + bytes_read; i++) {
if (read_buffer[i] == 0x00) {
if (++zero_counter == 8) {
// reset password
state->password = this->controller->get_password();
state->password_change = true;
Controller::process_password_change(state);
// drop input
size_t new_length = 0;
while (++i < read_buffer_len + bytes_read) {
read_buffer[new_length++] = read_buffer[i];
}
bytes_read = new_length;
read_buffer_len = 0;
log_info("api::serial", "session reset, remaining bytes: {} {}", bytes_read, bin2hex(read_buffer, bytes_read));
break;
}
} else {
zero_counter = 0;
}
}
}
// crypt in-data
if (state->cipher) {
state->cipher->crypt(&read_buffer[read_buffer_len], bytes_read);
}
// adjust size
read_buffer_len += bytes_read;
// check if message complete
for (size_t i = 0; i < read_buffer_len; ++i) {
if (read_buffer[i] != 0x00) {
continue;
} else {
// process request
std::vector<char> out;
if (!this->controller->process_request(
state,
(const char*) &read_buffer[0], read_buffer_len, &out)) {
// open new connection
log_warning("api::serial", "process error on {} (length {})",
this->port, i - 1);
this->free_port();
retry_time = 5;
break;
}
// adjust in-buffer
if (i == read_buffer_len - 1) {
read_buffer_len = 0;
} else {
size_t new_length = 0;
while (i < read_buffer_len) {
read_buffer[new_length++] = read_buffer[i++];
}
read_buffer_len = new_length;
}
// crypt out-data
if (state->cipher) {
state->cipher->crypt((uint8_t*) out.data(), out.size());
}
// send answer
DWORD bytes_written = 0;
if (!WriteFile(
this->handle,
out.data(),
out.size(),
&bytes_written,
nullptr) || bytes_written != out.size()) {
// open new connection
log_warning("api::serial", "write error on {}", this->port);
this->free_port();
break;
}
// check for password change
Controller::process_password_change(state);
//log_info("api::serial::out", "{}", bin2hex(out));
read_buffer_len = 0;
}
}
}
}
// slow down on reconnect
if (this->running) {
timer.sleep(retry_time);
}
}
});
}
SerialController::~SerialController() {
this->free_port();
this->running = false;
this->thread->join();
delete this->thread;
delete this->state;
}
void SerialController::open_port() {
// free resources
this->free_port();
// open port
this->handle = CreateFile(
this->port.c_str(),
GENERIC_READ | GENERIC_WRITE,
0,
nullptr,
OPEN_EXISTING,
0,
nullptr);
// check if open failed
if (this->handle == INVALID_HANDLE_VALUE) {
log_warning("api::serial", "failed to open port {}", port);
return;
}
// settings
DCB serial_params{};
serial_params.DCBlength = sizeof(serial_params);
if (!GetCommState(this->handle, &serial_params)) {
log_warning("api::serial", "{}: unable to get COM port state: 0x{:x}", port, GetLastError());
return;
}
// set params
serial_params.BaudRate = this->baud;
serial_params.ByteSize = 8;
serial_params.StopBits = ONESTOPBIT;
serial_params.Parity = NOPARITY;
if (!SetCommState(this->handle, &serial_params)) {
log_warning("api::serial", "{}: unable to set COM port state: 0x{:x}", port, GetLastError());
return;
}
// timeouts
COMMTIMEOUTS timeouts{};
timeouts.ReadIntervalTimeout = 5;
timeouts.ReadTotalTimeoutConstant = 0;
timeouts.ReadTotalTimeoutMultiplier = 0;
timeouts.WriteTotalTimeoutConstant = 30;
timeouts.WriteTotalTimeoutMultiplier = 5;
if (!SetCommTimeouts(this->handle, &timeouts)) {
log_warning("api::serial", "{}: unable to set COM port timeouts: 0x{:x}", port, GetLastError());
return;
}
// reset password
state->password_change = true;
Controller::process_password_change(state);
log_info("api::serial", "listening on {}/{}", port, baud);
}
void SerialController::free_port() {
if (this->handle != INVALID_HANDLE_VALUE) {
CloseHandle(this->handle);
this->handle = INVALID_HANDLE_VALUE;
}
}
}