Files
ARMSX2/pcsx2/DEV9/net.cpp
jpolo1224 069f8a44f3 Android 2.6.5.1: Local Link LAN play, async GS readback, pause/rotation/settings fixes
Crash and correctness
- Fix a crash when backgrounding the app mid-game: onPause flushed the Vulkan
  pipeline cache from the UI thread while the GS thread was creating pipelines
  into the same VkPipelineCache. Vulkan requires that handle to be externally
  synchronised, so this was a driver-level data race and crashed on Adreno and
  Xclipse alike. The flush now runs on the GS thread, posted via the CPU thread
  so it does not race the EE-owned MTGS ring.
- Fix an unbounded out-of-bounds vertex read in the GSRendererHW sprite-merge
  paving path: the inner loop advanced i instead of j, so j stayed loop-invariant
  and the scan walked past m_vertex->tail.
- Fix per-game settings being silently ignored: gamesettings/<serial>_<CRC>.ini
  loads into a higher-priority layer than anything the app writes, and saves made
  from the library never regenerated it, so any key already in that file
  overrode the user permanently. Only the category-Reset path rewrote it, which
  is why Reset appeared to be the only thing that worked.
- Fix screen rotation: the BIOS followed the launcher rotation instead of the
  renderer's (it has no GameInfo, and the tier was keyed on that), and the
  launcher stayed locked in a game's orientation after exit because the cleanup
  lived only inside stop()'s vmRunLoopActive-guarded branch, which loses a race
  against the VM thread's own finally. Rotation tier is now an explicit flag and
  the cleanup runs on every terminal path.
- Discard the Vulkan pipeline blob whenever the SPIR-V cache is discarded. It was
  validated only against the device header (vendor/device/pipelineCacheUUID),
  which is identical across an app update, so a SHADER_CACHE_VERSION bump kept
  every pipeline built from the old shaders and nothing pruned it.
- Make eeRecExitRequested atomic: it was a plain bool written from the JNI thread
  and read on the CPU thread.
- OpenGL: restore GL_PACK_ALIGNMENT after readback, add the missing memory
  barrier after the CAS dispatch, and initialise GLState::depth_mask to GL's
  actual default.
- DEV9: log the GetNetAdapter default: bail and the InitNet skip. Both returned
  silently, so a settings mistake surfaced as missing hardware three layers away.

Local Link (new)
- New DEV9 backend bridging emulated PS2 Ethernet between devices over
  authenticated local UDP, so games with a built-in LAN / System Link mode can
  play together. Ported from EmuCoreX (sashkinbro) with the wire format
  unchanged, so peers remain compatible across both forks.
- Network mode picker (Online / Host / Join), host address readout, auto-derived
  peer ids, generated room codes, hostname support alongside numeric IPv4, and a
  link to the supported-games list. Fully controller-navigable.

Performance
- Asynchronous hardware download mode (experimental, opt-in): non-blocking
  GPU->CPU readback so the EE thread no longer waits on the GS thread. Ported
  from EmuCoreX. Appending Asynchronous to GSHardwareDownloadMode makes the enum
  non-ordered, so the relational comparisons on it are replaced with
  IsHardwareDownloadReadbackEnabled / IsHardwareDownloadEEThreadRead.
- Affinity Control Mode (experimental, opt-in): EE/VU/GS priority orders plus a
  Performance Cores mode. Android otherwise leaves these threads unpinned.
- Raise the texture-replacement cache ceiling from 6 to 16 GB; RAM/2 remains the
  real limiter, so this only binds at 12 GB RAM and up.
- Low Latency frame pacing is no longer the default, with a one-time migration
  for installs that took the earlier flip.

Features
- Auto renderer resolves to Vulkan HW on Adreno.
- Auto Progressive Scan (per-game): holds Triangle+Cross through boot.
- OLED black as a modifier over any accent colour, including Custom and RGB.
- Optional system keyboard instead of the built-in on-screen one.

Game compatibility
- Everybody's Golf 4 / Hot Shots Golf Fore! hwDownloadMode across all regions
  (PR #421, XDarkFallenX).
- Delta Force: Black Hawk Down (PR #401, XDarkFallenX).
- Reduced input latency and input handling improvements (PR #403, Splaser).

RetroAchievements
- Inject the client version from a build-time secret kept out of public source,
  with a stock-PCSX2 fallback for secret-less builds, so third parties cannot
  copy the client identity. Covers the iOS token too.
2026-07-25 00:48:56 -04:00

469 lines
12 KiB
C++

// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include <chrono>
#include <thread>
#include <mutex>
#if defined(__POSIX__)
#include <pthread.h>
#endif
#include "net.h"
#include "DEV9.h"
#ifdef _WIN32
#include "Win32/tap.h"
#endif
#include "pcap_io.h"
#include "sockets.h"
#include "LocalLinkAdapter.h"
#include "PacketReader/EthernetFrame.h"
#include "PacketReader/IP/IP_Packet.h"
#include "PacketReader/IP/UDP/UDP_Packet.h"
NetAdapter* nif;
std::thread rx_thread;
std::mutex rx_mutex;
volatile bool RxRunning = false;
//rx thread
void NetRxThread()
{
NetPacket tmp;
while (RxRunning)
{
while (rx_fifo_can_rx() && nif->recv(&tmp))
{
std::lock_guard rx_lock(rx_mutex);
//Check if we can still rx
if (rx_fifo_can_rx())
rx_process(&tmp);
else
Console.Error("DEV9: rx_fifo_can_rx() false after nif->recv(), dropping");
}
using namespace std::chrono_literals;
std::this_thread::sleep_for(1ms);
}
}
void tx_put(NetPacket* pkt)
{
if (nif != nullptr)
nif->send(pkt);
//pkt must be copied if its not processed by here, since it can be allocated on the callers stack
}
void ad_reset()
{
if (nif != nullptr)
nif->reset();
}
NetAdapter* GetNetAdapter()
{
NetAdapter* na = nullptr;
switch (EmuConfig.DEV9.EthApi)
{
#ifdef _WIN32
case Pcsx2Config::DEV9Options::NetApi::TAP:
na = static_cast<NetAdapter*>(new TAPAdapter());
break;
#endif
case Pcsx2Config::DEV9Options::NetApi::PCAP_Bridged:
case Pcsx2Config::DEV9Options::NetApi::PCAP_Switched:
na = static_cast<NetAdapter*>(new PCAPAdapter());
break;
case Pcsx2Config::DEV9Options::NetApi::Sockets:
na = static_cast<NetAdapter*>(new SocketAdapter());
break;
case Pcsx2Config::DEV9Options::NetApi::LocalLink:
na = static_cast<NetAdapter*>(new LocalLinkAdapter());
break;
default:
// Used to `return 0` in silence, which meant a mis-set or overridden EthApi produced no
// log line anywhere — the emulator simply had no network adapter and the GAME reported
// "network adaptor is not connected" three layers away. Name the value.
Console.Error("DEV9: no network backend for EthApi=%d, Ethernet stays off (a stale "
"per-game gamesettings INI can override your choice here)",
static_cast<int>(EmuConfig.DEV9.EthApi));
return 0;
}
if (!na->isInitialised())
{
// Likewise: the backend's own constructor logs why it bailed, but without this line there
// is nothing tying that failure to "and therefore you have no adapter at all".
Console.Error("DEV9: network backend for EthApi=%d failed to initialise, Ethernet stays off",
static_cast<int>(EmuConfig.DEV9.EthApi));
delete na;
return 0;
}
return na;
}
void InitNet()
{
NetAdapter* na = GetNetAdapter();
if (!na)
{
Console.Error("DEV9: Failed to GetNetAdapter()");
EmuConfig.DEV9.EthEnable = false;
return;
}
nif = na;
RxRunning = true;
rx_thread = std::thread(NetRxThread);
#ifdef _WIN32
SetThreadPriority(rx_thread.native_handle(), THREAD_PRIORITY_HIGHEST);
#elif defined(__POSIX__)
int policy = 0;
sched_param param;
pthread_getschedparam(rx_thread.native_handle(), &policy, &param);
param.sched_priority = sched_get_priority_max(policy);
pthread_setschedparam(rx_thread.native_handle(), policy, &param);
#endif
}
void ReconfigureLiveNet(const Pcsx2Config& old_config)
{
//Eth
if (EmuConfig.DEV9.EthEnable)
{
if (old_config.DEV9.EthEnable)
{
//Reload Net if adapter changed
if (EmuConfig.DEV9.EthDevice != old_config.DEV9.EthDevice ||
EmuConfig.DEV9.EthApi != old_config.DEV9.EthApi ||
EmuConfig.DEV9.LocalLinkHost != old_config.DEV9.LocalLinkHost ||
EmuConfig.DEV9.LocalLinkAddress != old_config.DEV9.LocalLinkAddress ||
EmuConfig.DEV9.LocalLinkPort != old_config.DEV9.LocalLinkPort ||
EmuConfig.DEV9.LocalLinkPeerId != old_config.DEV9.LocalLinkPeerId ||
EmuConfig.DEV9.LocalLinkRoomCode != old_config.DEV9.LocalLinkRoomCode)
{
TermNet();
InitNet();
return;
}
else
nif->reloadSettings();
}
else
InitNet();
}
else if (old_config.DEV9.EthEnable)
TermNet();
}
void TermNet()
{
if (RxRunning)
{
RxRunning = false;
nif->close();
Console.WriteLn("DEV9: Waiting for RX-net thread to terminate..");
rx_thread.join();
Console.WriteLn("DEV9: Done");
delete nif;
nif = nullptr;
}
}
using namespace PacketReader;
using namespace PacketReader::IP;
using namespace PacketReader::IP::UDP;
const IP_Address NetAdapter::internalIP{{{192, 0, 2, 1}}};
const MAC_Address NetAdapter::broadcastMAC{{{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}}};
const MAC_Address NetAdapter::internalMAC{{{0x76, 0x6D, 0xF4, 0x63, 0x30, 0x31}}};
NetAdapter::NetAdapter()
{
//Ensure eeprom matches our default
SetMACAddress(nullptr);
}
bool NetAdapter::recv(NetPacket* pkt)
{
if (!internalRxThreadRunning.load())
return InternalServerRecv(pkt);
return false;
}
bool NetAdapter::send(NetPacket* pkt)
{
return InternalServerSend(pkt);
}
//RxRunning must be set false before this
NetAdapter::~NetAdapter()
{
//unblock InternalServerRX thread
if (internalRxThreadRunning.load())
{
internalRxThreadRunning.store(false);
{
std::lock_guard srvlock(internalRxMutex);
internalRxHasData = true;
}
internalRxCV.notify_all();
internalRxThread.join();
}
}
void NetAdapter::InspectSend(NetPacket* pkt)
{
if (EmuConfig.DEV9.EthLogDNS || EmuConfig.DEV9.EthLogDHCP)
{
EthernetFrame frame(pkt);
if (frame.protocol == static_cast<u16>(EtherType::IPv4))
{
PayloadPtr* payload = static_cast<PayloadPtr*>(frame.GetPayload());
IP_Packet ippkt(payload->data, payload->GetLength());
if (ippkt.protocol == static_cast<u16>(IP_Type::UDP))
{
IP_PayloadPtr* ipPayload = static_cast<IP_PayloadPtr*>(ippkt.GetPayload());
UDP_Packet udppkt(ipPayload->data, ipPayload->GetLength());
if (EmuConfig.DEV9.EthLogDNS && udppkt.destinationPort == 53)
{
Console.WriteLn("DEV9: DNS: Packet Sent To %i.%i.%i.%i",
ippkt.destinationIP.bytes[0], ippkt.destinationIP.bytes[1], ippkt.destinationIP.bytes[2], ippkt.destinationIP.bytes[3]);
dnsLogger.InspectSend(&udppkt);
}
if (EmuConfig.DEV9.EthLogDHCP && udppkt.destinationPort == 67)
{
Console.WriteLn("DEV9: DHCP: Packet Sent To %i.%i.%i.%i",
ippkt.destinationIP.bytes[0], ippkt.destinationIP.bytes[1], ippkt.destinationIP.bytes[2], ippkt.destinationIP.bytes[3]);
dhcpLogger.InspectSend(&udppkt);
}
}
}
}
}
void NetAdapter::InspectRecv(NetPacket* pkt)
{
if (EmuConfig.DEV9.EthLogDNS || EmuConfig.DEV9.EthLogDHCP)
{
EthernetFrame frame(pkt);
if (frame.protocol == static_cast<u16>(EtherType::IPv4))
{
PayloadPtr* payload = static_cast<PayloadPtr*>(frame.GetPayload());
IP_Packet ippkt(payload->data, payload->GetLength());
if (ippkt.protocol == static_cast<u16>(IP_Type::UDP))
{
IP_PayloadPtr* ipPayload = static_cast<IP_PayloadPtr*>(ippkt.GetPayload());
UDP_Packet udppkt(ipPayload->data, ipPayload->GetLength());
if (EmuConfig.DEV9.EthLogDNS && udppkt.sourcePort == 53)
{
Console.WriteLn("DEV9: DNS: Packet Sent From %i.%i.%i.%i",
ippkt.sourceIP.bytes[0], ippkt.sourceIP.bytes[1], ippkt.sourceIP.bytes[2], ippkt.sourceIP.bytes[3]);
dnsLogger.InspectRecv(&udppkt);
}
if (EmuConfig.DEV9.EthLogDHCP && udppkt.sourcePort == 67)
{
Console.WriteLn("DEV9: DHCP: Packet Sent From %i.%i.%i.%i",
ippkt.sourceIP.bytes[0], ippkt.sourceIP.bytes[1], ippkt.sourceIP.bytes[2], ippkt.sourceIP.bytes[3]);
dhcpLogger.InspectRecv(&udppkt);
}
}
}
}
}
void NetAdapter::SetMACAddress(MAC_Address* mac)
{
if (mac == nullptr)
ps2MAC = defaultMAC;
else
ps2MAC = *mac;
*(MAC_Address*)&dev9.eeprom[0] = ps2MAC;
//The checksum seems to be all the values of the mac added up in 16bit chunks
dev9.eeprom[3] = (dev9.eeprom[0] + dev9.eeprom[1] + dev9.eeprom[2]) & 0xffff;
}
bool NetAdapter::VerifyPkt(NetPacket* pkt, int read_size)
{
if ((*(MAC_Address*)&pkt->buffer[0] != ps2MAC) && (*(MAC_Address*)&pkt->buffer[0] != broadcastMAC))
{
//ignore strange packets
return false;
}
if (*(MAC_Address*)&pkt->buffer[6] == ps2MAC)
{
//avoid pcap looping packets
return false;
}
pkt->size = read_size;
return true;
}
#ifdef _WIN32
void NetAdapter::InitInternalServer(PIP_ADAPTER_ADDRESSES adapter, bool dhcpForceEnable, IP_Address ipOverride, IP_Address subnetOverride, IP_Address gatewayOvveride)
#elif defined(__POSIX__)
void NetAdapter::InitInternalServer(ifaddrs* adapter, bool dhcpForceEnable, IP_Address ipOverride, IP_Address subnetOverride, IP_Address gatewayOvveride)
#endif
{
if (adapter == nullptr)
Console.Error("DEV9: InitInternalServer() got nullptr for adapter");
dhcpLogger.Init(adapter);
dhcpOn = EmuConfig.DEV9.InterceptDHCP || dhcpForceEnable;
if (dhcpOn)
dhcpServer.Init(adapter, ipOverride, subnetOverride, gatewayOvveride);
dnsServer.Init(adapter);
if (blocks())
{
internalRxThreadRunning.store(true);
internalRxThread = std::thread(&NetAdapter::InternalServerThread, this);
}
}
#ifdef _WIN32
void NetAdapter::ReloadInternalServer(PIP_ADAPTER_ADDRESSES adapter, bool dhcpForceEnable, IP_Address ipOverride, IP_Address subnetOverride, IP_Address gatewayOveride)
#elif defined(__POSIX__)
void NetAdapter::ReloadInternalServer(ifaddrs* adapter, bool dhcpForceEnable, IP_Address ipOverride, IP_Address subnetOverride, IP_Address gatewayOveride)
#endif
{
if (adapter == nullptr)
Console.Error("DEV9: ReloadInternalServer() got nullptr for adapter");
dhcpOn = EmuConfig.DEV9.InterceptDHCP || dhcpForceEnable;
if (dhcpOn)
dhcpServer.Init(adapter, ipOverride, subnetOverride, gatewayOveride);
dnsServer.Init(adapter);
}
bool NetAdapter::InternalServerRecv(NetPacket* pkt)
{
IP_Payload* ippay;
ippay = dhcpServer.Recv();
if (ippay != nullptr)
{
IP_Packet* ippkt = new IP_Packet(ippay);
ippkt->destinationIP = {{{255, 255, 255, 255}}};
ippkt->sourceIP = internalIP;
EthernetFrame frame(ippkt);
frame.sourceMAC = internalMAC;
frame.destinationMAC = ps2MAC;
frame.protocol = static_cast<u16>(EtherType::IPv4);
frame.WritePacket(pkt);
InspectRecv(pkt);
return true;
}
ippay = dnsServer.Recv();
if (ippay != nullptr)
{
IP_Packet* ippkt = new IP_Packet(ippay);
ippkt->destinationIP = ps2IP;
ippkt->sourceIP = internalIP;
EthernetFrame frame(ippkt);
frame.sourceMAC = internalMAC;
frame.destinationMAC = ps2MAC;
frame.protocol = static_cast<u16>(EtherType::IPv4);
frame.WritePacket(pkt);
InspectRecv(pkt);
return true;
}
return false;
}
bool NetAdapter::InternalServerSend(NetPacket* pkt)
{
EthernetFrame frame(pkt);
if (frame.protocol == static_cast<u16>(EtherType::IPv4))
{
PayloadPtr* payload = static_cast<PayloadPtr*>(frame.GetPayload());
IP_Packet ippkt(payload->data, payload->GetLength());
if (ippkt.protocol == static_cast<u16>(IP_Type::UDP))
{
IP_PayloadPtr* ipPayload = static_cast<IP_PayloadPtr*>(ippkt.GetPayload());
UDP_Packet udppkt(ipPayload->data, ipPayload->GetLength());
if (udppkt.destinationPort == 67)
{
//Send DHCP
if (dhcpOn)
return dhcpServer.Send(&udppkt);
}
}
if (ippkt.destinationIP == internalIP)
{
if (ippkt.protocol == static_cast<u16>(IP_Type::UDP))
{
ps2IP = ippkt.sourceIP;
IP_PayloadPtr* ipPayload = static_cast<IP_PayloadPtr*>(ippkt.GetPayload());
UDP_Packet udppkt(ipPayload->data, ipPayload->GetLength());
if (udppkt.destinationPort == 53)
{
//Send DNS
return dnsServer.Send(&udppkt);
}
}
return true;
}
}
return false;
}
void NetAdapter::InternalSignalReceived()
{
//Signal internal server thread to read
if (internalRxThreadRunning.load())
{
{
std::lock_guard srvlock(internalRxMutex);
internalRxHasData = true;
}
internalRxCV.notify_all();
}
}
void NetAdapter::InternalServerThread()
{
NetPacket tmp;
while (internalRxThreadRunning.load())
{
std::unique_lock srvLock(internalRxMutex);
internalRxCV.wait(srvLock, [&] { return internalRxHasData; });
{
std::lock_guard rx_lock(rx_mutex);
while (rx_fifo_can_rx() && InternalServerRecv(&tmp))
rx_process(&tmp);
}
internalRxHasData = false;
}
}