mirror of
https://github.com/lucannez64/darling-lol.git
synced 2026-10-26 19:47:42 +00:00
Game process (ARAM Hexgate: 84 -> 127 FPS average, slowest stretches ~110): - Local pthread mutexes, condition variables and Mach semaphores are on by default in the game process only, with /tmp/rclol-no-local-* opt-outs. The main thread's darlingserver waits fell from 1.36 to 0.10 ms per frame. - Local semaphores: the count and the slot generation change in one compare-exchange, a destroy leaves a negative marker, the table is reset after fork, and names the table did not create go to the server. - Metal: drop identical re-binds (keyed on the Indium object, depth-stencil retained), pass blit resources by reference, and stop resetting vertex descriptors twice. - Core isolation of the main thread is opt-in (/tmp/rclol-core-isolation); it lowered FPS in its only match. - Optimized libc/libc++/Foundation overlays, AVX2 string primitives, real CPU topology and their installers and offline checks. Tools and docs: - tools/riot-match-capture: passive /proc sampler, user-space IP profiler and reports, plus a comparison against the game's netstats. - tools/riot-lsem-model and tools/riot-metal-bind-model: host models that extract the shipped code and check it. - tools/install-riot-metal-pair.sh installs Metal and Indium together. - Status, setup, testing and troubleshooting docs updated with the measured matches and the switch table; manifest file counts match the patches. Co-Authored-By: Claude Opus 5.5 <[email protected]>
341 lines
12 KiB
C++
341 lines
12 KiB
C++
// Host-side checks for the reply path (MessageQueue::pushAndSend): direct sends
|
|
// from the producing thread, FIFO order under concurrency and EAGAIN, wakeup
|
|
// notification rules, descriptor ownership and dead-peer handling.
|
|
//
|
|
// Standalone host sockets only. Never connects to Darling, Riot or the game.
|
|
#include <darlingserver/message.hpp>
|
|
#include <sys/socket.h>
|
|
#include <sys/un.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
#include <dirent.h>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <stdexcept>
|
|
#include <system_error>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <vector>
|
|
#include <algorithm>
|
|
|
|
using DarlingServer::Address;
|
|
using DarlingServer::Message;
|
|
using DarlingServer::MessageQueue;
|
|
|
|
static void require(bool pass, const char* what) {
|
|
if (!pass) throw std::runtime_error(what);
|
|
}
|
|
|
|
static unsigned openFDs() {
|
|
DIR* directory = opendir("/proc/self/fd");
|
|
require(directory, "fd directory");
|
|
unsigned n = 0;
|
|
while (auto entry = readdir(directory)) if (entry->d_name[0] != '.') ++n;
|
|
closedir(directory);
|
|
return n;
|
|
}
|
|
|
|
// A guest-side socket with its own bound address, like a guest RPC endpoint.
|
|
struct Peer {
|
|
int fd = -1;
|
|
std::string path;
|
|
explicit Peer(unsigned index) {
|
|
fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
|
|
require(fd >= 0, "peer socket");
|
|
path = "/tmp/opencode/riot-host-reply-check-" + std::to_string(getpid()) + "-" + std::to_string(index);
|
|
unlink(path.c_str());
|
|
sockaddr_un address{};
|
|
address.sun_family = AF_UNIX;
|
|
std::strncpy(address.sun_path, path.c_str(), sizeof(address.sun_path) - 1);
|
|
require(bind(fd, reinterpret_cast<sockaddr*>(&address), sizeof(address)) == 0, "peer bind");
|
|
}
|
|
~Peer() {
|
|
if (fd >= 0) close(fd);
|
|
if (!path.empty()) unlink(path.c_str());
|
|
}
|
|
sockaddr_un address() const {
|
|
sockaddr_un address{};
|
|
address.sun_family = AF_UNIX;
|
|
std::strncpy(address.sun_path, path.c_str(), sizeof(address.sun_path) - 1);
|
|
return address;
|
|
}
|
|
};
|
|
|
|
// The server's listener socket; a small send buffer forces EAGAIN quickly.
|
|
static int serverSocket(int sendBuffer = 0) {
|
|
int fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
|
|
require(fd >= 0, "server socket");
|
|
if (sendBuffer) {
|
|
require(setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &sendBuffer, sizeof(sendBuffer)) == 0, "send buffer");
|
|
}
|
|
return fd;
|
|
}
|
|
|
|
static Message packetFor(const Peer& peer, uint64_t tag, size_t size = 64, unsigned descriptors = 0) {
|
|
Message message(size, descriptors);
|
|
std::memcpy(message.data().data(), &tag, sizeof(tag));
|
|
message.setAddress(Address(peer.address()));
|
|
return message;
|
|
}
|
|
|
|
struct Incoming {
|
|
bool valid = false;
|
|
uint64_t tag = 0;
|
|
size_t size = 0;
|
|
std::vector<int> fds;
|
|
};
|
|
|
|
static Incoming receive(int fd) {
|
|
Incoming incoming;
|
|
unsigned char buffer[512];
|
|
unsigned char control[CMSG_SPACE(sizeof(int) * 8)];
|
|
struct iovec iov{ buffer, sizeof(buffer) };
|
|
struct msghdr header{};
|
|
header.msg_iov = &iov;
|
|
header.msg_iovlen = 1;
|
|
header.msg_control = control;
|
|
header.msg_controllen = sizeof(control);
|
|
ssize_t received = recvmsg(fd, &header, 0);
|
|
if (received < 0) {
|
|
if (errno == EAGAIN || errno == EWOULDBLOCK) return incoming;
|
|
throw std::system_error(errno, std::generic_category(), "recvmsg");
|
|
}
|
|
incoming.valid = true;
|
|
incoming.size = static_cast<size_t>(received);
|
|
if (received >= static_cast<ssize_t>(sizeof(incoming.tag))) std::memcpy(&incoming.tag, buffer, sizeof(incoming.tag));
|
|
for (auto cmsg = CMSG_FIRSTHDR(&header); cmsg; cmsg = CMSG_NXTHDR(&header, cmsg)) {
|
|
if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_RIGHTS) {
|
|
size_t count = (cmsg->cmsg_len - CMSG_LEN(0)) / sizeof(int);
|
|
for (size_t i = 0; i < count; ++i) {
|
|
int descriptor = -1;
|
|
std::memcpy(&descriptor, CMSG_DATA(cmsg) + (sizeof(int) * i), sizeof(int));
|
|
incoming.fds.push_back(descriptor);
|
|
}
|
|
}
|
|
}
|
|
return incoming;
|
|
}
|
|
|
|
// Reads everything currently queued on the peer, in arrival order.
|
|
static void receiveAll(int fd, std::vector<uint64_t>& tags, std::vector<std::vector<int>>& descriptors) {
|
|
while (true) {
|
|
auto incoming = receive(fd);
|
|
if (!incoming.valid) return;
|
|
tags.push_back(incoming.tag);
|
|
descriptors.push_back(std::move(incoming.fds));
|
|
}
|
|
}
|
|
|
|
// The main loop's job: send what the queue can send, then let the peer consume it.
|
|
static void drainQueue(MessageQueue& queue, int serverFD, int peerFD, std::vector<uint64_t>& tags, std::vector<std::vector<int>>& descriptors, size_t expected) {
|
|
for (unsigned spin = 0; tags.size() < expected; ++spin) {
|
|
require(spin < 20000, "drain made no progress");
|
|
queue.sendMany(serverFD);
|
|
receiveAll(peerFD, tags, descriptors);
|
|
if (tags.size() < expected) std::this_thread::sleep_for(std::chrono::microseconds(50));
|
|
}
|
|
}
|
|
|
|
static void checkWakeupRules() {
|
|
int server = serverSocket(4096);
|
|
Peer peer(0);
|
|
MessageQueue queue;
|
|
unsigned callbacks = 0;
|
|
queue.setMessageArrivalNotificationCallback([&] {
|
|
++callbacks;
|
|
require(!queue.empty(), "callback fired on an empty queue");
|
|
});
|
|
|
|
// A direct send that drains the queue must not wake the main loop.
|
|
queue.pushAndSend(packetFor(peer, 1000), server);
|
|
require(callbacks == 0, "drained direct send notified");
|
|
require(queue.empty(), "drained direct send left messages");
|
|
auto first = receive(peer.fd);
|
|
require(first.valid && first.tag == 1000, "direct send did not arrive");
|
|
|
|
// The legacy push path still notifies unconditionally.
|
|
queue.push(packetFor(peer, 1001));
|
|
require(callbacks == 1, "push did not notify");
|
|
require(!queue.empty(), "push did not queue");
|
|
|
|
// Under EAGAIN the queue remains and the main loop is notified.
|
|
std::vector<uint64_t> tags;
|
|
std::vector<std::vector<int>> descriptors;
|
|
for (uint64_t i = 0; i < 64; ++i) queue.pushAndSend(packetFor(peer, 2000 + i), server);
|
|
require(callbacks >= 2, "EAGAIN did not notify");
|
|
require(!queue.empty(), "EAGAIN did not leave messages queued");
|
|
unsigned afterEagain = callbacks;
|
|
|
|
drainQueue(queue, server, peer.fd, tags, descriptors, 64 + 1);
|
|
require(queue.empty(), "drain left messages");
|
|
require(tags.size() == 65, "wrong message count");
|
|
require(tags[0] == 1001, "queued message lost");
|
|
for (size_t i = 0; i < 64; ++i) require(tags[i + 1] == 2000 + i, "EAGAIN retry reordered or lost a message");
|
|
require(callbacks == afterEagain, "drain notified without a push");
|
|
|
|
close(server);
|
|
}
|
|
|
|
static void checkConcurrentPushers() {
|
|
int server = serverSocket(4096); // small buffer: mix direct sends and the retry path
|
|
Peer peer(1);
|
|
MessageQueue queue;
|
|
std::atomic<unsigned> callbacks{0};
|
|
|
|
constexpr unsigned pusherCount = 4;
|
|
constexpr unsigned perPusher = 2000;
|
|
constexpr size_t total = pusherCount * perPusher;
|
|
std::atomic<unsigned> ready{0};
|
|
std::vector<std::thread> pushers;
|
|
// Under concurrency the queue may be drained between the notification and this
|
|
// callback, so only its firing is observable here (like the server's eventfd).
|
|
queue.setMessageArrivalNotificationCallback([&] { ++callbacks; });
|
|
for (unsigned p = 0; p < pusherCount; ++p) {
|
|
pushers.emplace_back([&, p] {
|
|
++ready;
|
|
for (uint64_t i = 0; i < perPusher; ++i) {
|
|
queue.pushAndSend(packetFor(peer, (static_cast<uint64_t>(p) << 32) | i), server);
|
|
}
|
|
});
|
|
}
|
|
while (ready.load() != pusherCount) std::this_thread::yield();
|
|
|
|
// The main loop's role: send whatever the pushers had to leave behind.
|
|
std::vector<unsigned> nextSequence(pusherCount, 0);
|
|
size_t received = 0;
|
|
for (unsigned spin = 0; received < total; ++spin) {
|
|
require(spin < 400000, "concurrent drain made no progress");
|
|
queue.sendMany(server);
|
|
while (true) {
|
|
auto incoming = receive(peer.fd);
|
|
if (!incoming.valid) break;
|
|
unsigned p = incoming.tag >> 32;
|
|
unsigned sequence = incoming.tag & 0xffffffff;
|
|
require(p < pusherCount, "unknown pusher");
|
|
require(sequence == nextSequence[p], "message reordered for one pusher");
|
|
++nextSequence[p];
|
|
++received;
|
|
}
|
|
if (received < total) std::this_thread::sleep_for(std::chrono::microseconds(50));
|
|
}
|
|
|
|
for (auto& pusher : pushers) pusher.join();
|
|
require(received == total, "lost or duplicated messages");
|
|
for (unsigned p = 0; p < pusherCount; ++p) require(nextSequence[p] == perPusher, "incomplete pusher sequence");
|
|
|
|
close(server);
|
|
}
|
|
|
|
static void checkDescriptors() {
|
|
int server = serverSocket(65536);
|
|
Peer peer(2);
|
|
MessageQueue queue;
|
|
unsigned baseline = openFDs();
|
|
|
|
for (uint64_t i = 0; i < 8; ++i) {
|
|
Message message = packetFor(peer, 3000 + i, 64, 2);
|
|
for (unsigned d = 0; d < 2; ++d) {
|
|
int pipeFDs[2];
|
|
require(pipe(pipeFDs) == 0, "pipe");
|
|
unsigned char byte = static_cast<unsigned char>(i * 2 + d);
|
|
require(write(pipeFDs[1], &byte, 1) == 1, "pipe write");
|
|
close(pipeFDs[1]);
|
|
message.pushDescriptor(pipeFDs[0]);
|
|
}
|
|
queue.pushAndSend(std::move(message), server);
|
|
// the queue drops its message (and its descriptors) right after the send
|
|
require(openFDs() == baseline, "descriptor not closed on the sender");
|
|
}
|
|
|
|
std::vector<uint64_t> tags;
|
|
std::vector<std::vector<int>> descriptors;
|
|
receiveAll(peer.fd, tags, descriptors);
|
|
require(tags.size() == 8, "wrong descriptor message count");
|
|
for (size_t i = 0; i < tags.size(); ++i) {
|
|
require(tags[i] == 3000 + i, "descriptor message order");
|
|
require(descriptors[i].size() == 2, "descriptor count");
|
|
for (unsigned d = 0; d < 2; ++d) {
|
|
unsigned char byte = 0;
|
|
require(read(descriptors[i][d], &byte, 1) == 1, "descriptor read");
|
|
require(byte == static_cast<unsigned char>(i * 2 + d), "descriptor payload");
|
|
close(descriptors[i][d]);
|
|
}
|
|
}
|
|
require(openFDs() == baseline, "received descriptors leaked");
|
|
|
|
close(server);
|
|
}
|
|
|
|
static void checkDeadPeer() {
|
|
int server = serverSocket(0);
|
|
MessageQueue queue;
|
|
Peer live(3);
|
|
unsigned baseline = openFDs();
|
|
|
|
// A peer that unbound its address makes the kernel report ENOENT.
|
|
{
|
|
Peer dead(4);
|
|
sockaddr_un address = dead.address();
|
|
close(dead.fd);
|
|
dead.fd = -1;
|
|
unlink(dead.path.c_str());
|
|
Message message(64, 0);
|
|
message.setAddress(Address(address));
|
|
queue.pushAndSend(std::move(message), server); // must not throw
|
|
}
|
|
|
|
// A peer that closed its (still-bound) address reports ECONNREFUSED.
|
|
{
|
|
Peer dead(5);
|
|
sockaddr_un address = dead.address();
|
|
Message message(64, 0);
|
|
message.setAddress(Address(address));
|
|
close(dead.fd);
|
|
dead.fd = -1;
|
|
queue.pushAndSend(std::move(message), server); // must not throw
|
|
}
|
|
|
|
require(queue.empty(), "dead peer wedged the queue");
|
|
require(openFDs() == baseline, "dead peer leaked descriptors");
|
|
|
|
// Two dropped messages in front of a live one must not block or reorder it.
|
|
sockaddr_un deadAddress{};
|
|
{
|
|
Peer dead(6);
|
|
deadAddress = dead.address();
|
|
close(dead.fd);
|
|
dead.fd = -1;
|
|
}
|
|
for (unsigned i = 0; i < 2; ++i) {
|
|
Message message(64, 0);
|
|
message.setAddress(Address(deadAddress));
|
|
queue.pushAndSend(std::move(message), server);
|
|
}
|
|
require(queue.empty(), "dead peer messages remained queued");
|
|
queue.pushAndSend(packetFor(live, 4000), server);
|
|
|
|
std::vector<uint64_t> tags;
|
|
std::vector<std::vector<int>> descriptors;
|
|
receiveAll(live.fd, tags, descriptors);
|
|
require(tags.size() == 1 && tags[0] == 4000, "live message after dead peers");
|
|
|
|
close(server);
|
|
}
|
|
|
|
int main() {
|
|
try {
|
|
checkWakeupRules();
|
|
checkConcurrentPushers();
|
|
checkDescriptors();
|
|
checkDeadPeer();
|
|
std::printf("HOST_REPLY pass=1 direct_send=ok eagain_retry=ok fifo=ok concurrency=ok notify=ok descriptors=ok dead_peer=ok\n");
|
|
return 0;
|
|
} catch (const std::exception& error) {
|
|
std::fprintf(stderr, "HOST_REPLY pass=0 error=%s\n", error.what());
|
|
return 1;
|
|
}
|
|
}
|