Files
ARMSX2/pcsx2/DEV9/LocalLinkAdapter.h
T
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

127 lines
3.5 KiB
C++

// SPDX-FileCopyrightText: 2026 EmuCoreX contributors
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "net.h"
#include <array>
#include <atomic>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <mutex>
#include <unordered_map>
#include <string>
#include <vector>
#ifdef _WIN32
#include "common/RedtapeWindows.h"
#include <winsock2.h>
#else
#include <netinet/in.h>
#endif
// A small authenticated layer-2 tunnel intended for phones on the same Wi-Fi
// network or local hotspot. The host is a star-topology Ethernet relay; it is
// deliberately separate from the internet-facing DEV9 Sockets backend.
class LocalLinkAdapter final : public NetAdapter
{
public:
LocalLinkAdapter();
~LocalLinkAdapter() override;
bool blocks() override;
bool isInitialised() override;
bool recv(NetPacket* pkt) override;
bool send(NetPacket* pkt) override;
void reloadSettings() override;
void close() override;
private:
static constexpr std::size_t MAX_FRAGMENT_PAYLOAD = 1000;
static constexpr std::size_t MAX_PEERS = 8;
struct Peer
{
sockaddr_in endpoint{};
u32 id = 0;
std::chrono::steady_clock::time_point last_seen{};
u32 highest_sequence = 0;
u64 replay_window = 0;
};
struct ReassemblyKey
{
u32 peer_id;
u32 frame_id;
bool operator==(const ReassemblyKey& rhs) const
{
return peer_id == rhs.peer_id && frame_id == rhs.frame_id;
}
};
struct ReassemblyKeyHash
{
std::size_t operator()(const ReassemblyKey& key) const
{
return (static_cast<std::size_t>(key.peer_id) << 32) ^ key.frame_id;
}
};
struct Reassembly
{
std::array<u8, 1514> data{};
std::array<u16, 2> sizes{};
std::array<bool, 2> received{};
u16 fragment_count = 0;
std::chrono::steady_clock::time_point created{};
};
#ifdef _WIN32
SOCKET m_socket = INVALID_SOCKET;
bool m_wsa_started = false;
#else
int m_socket = -1;
#endif
std::atomic<bool> m_initialized{false};
bool m_host = false;
std::atomic<bool> m_closed{false};
u16 m_port = 19072;
u32 m_peer_id = 1;
std::atomic<u32> m_send_sequence{1};
std::atomic<u32> m_frame_id{1};
std::atomic<u64> m_session_nonce{0};
u64 m_auth_key0 = 0;
u64 m_auth_key1 = 0;
sockaddr_in m_host_endpoint{};
std::chrono::steady_clock::time_point m_last_hello{};
std::mutex m_peer_mutex;
std::mutex m_socket_mutex;
std::vector<Peer> m_peers;
std::unordered_map<u32, Peer> m_remote_peers;
std::unordered_map<ReassemblyKey, Reassembly, ReassemblyKeyHash> m_reassembly;
bool OpenSocket();
bool ConfigureEndpoint();
void SendHelloIfNeeded(bool force = false);
void SendControl(u8 type, const sockaddr_in& endpoint);
bool SendFrameFragments(const NetPacket& pkt, const sockaddr_in& endpoint);
bool SendDatagram(u8 type, u32 source_peer, u32 frame_id, u16 fragment_index,
u16 fragment_count, const void* payload, u16 payload_size, const sockaddr_in& endpoint);
bool ReceiveDatagram(NetPacket* pkt, bool* had_datagram);
void RelayDatagram(const void* data, std::size_t size, const sockaddr_in& source);
Peer* FindPeer(u32 id, const sockaddr_in& endpoint);
bool RegisterPeer(u32 id, u32 hello_sequence, const sockaddr_in& endpoint);
bool AcceptSequence(Peer& peer, u32 sequence);
bool VerifyLocalLinkPacket(NetPacket* pkt, int read_size);
void PurgeExpiredState();
static bool SameEndpoint(const sockaddr_in& lhs, const sockaddr_in& rhs);
static u64 DeriveKey(const std::string& room_code, u64 salt);
static u64 Authenticate(const void* header, std::size_t header_size,
const void* payload, std::size_t payload_size, u64 key0, u64 key1);
};