Files
Henrik RydgårdandClaude Opus 5 2cb0be1bc4 UPnP: fix the exit hang and the CPU spin, and only run the thread when enabled
The two long-standing bug reports had a shared root: the service loop could
end up in a state it never left.

- Exit hang: UPNP_CMD_EXIT was queued alongside port requests and only acted
  on when it reached the front. A request that couldn't complete was never
  popped, so exit sat behind it forever and join() blocked indefinitely.
  Exit is a flag now, checked before anything else.
- CPU spin: wait_for() with a predicate returns immediately when the predicate
  already holds, so a stuck queue head meant a tight loop. sceNetInet's bind()
  queues UPnP_Add regardless of the setting, so this hit whenever UPnP was off
  and a game used sockets. The loop always blocks now, and requests are dropped
  while UPnP is off.
- Failed discovery was retried every 5s forever, each time a full 2s SSDP round
  plus an error toast. Now backs off 5s -> 300s and reports once.

Other things found while in here:

- Every failed Initialize() leaked a UPNPUrls + IGDdatas, so ~every 5 seconds
  for anyone with UPnP on and no router. The manual miniwget/parserootdesc/
  GetUPNPUrls block was also redundant - UPNP_GetValidIGD does all of it and
  memsets over the result, leaking the URLs and costing an extra HTTP round
  trip per attempt.
- UPNP_GetValidIGD's status was never checked, so we could go DONE with no
  usable IGD and hand a NULL controlURL to UPNP_GetConnectionTypeInfo.
- miniupnpc's strncpy into the port-mapping-entry buffers doesn't guarantee a
  terminator; an 80-char description ran std::string off the end of desc[80].
- Add() marked another app's port "taken" only after our own add succeeded, so
  a failed add left their mapping deleted and never restored.
- Clear() walked the router's entire table at exit, one HTTP round trip per
  index. It now deletes only what we know we mapped, and the exit cleanup has
  a time budget so an unreachable router can't stall shutdown.
- The in-flight request stayed in the queue during the router call, so a
  same-port request arriving concurrently could erase it and be dropped
  unexecuted.
- The queue is bounded, and last-write-wins per port collapses the churn from
  games that rebind in a loop.
- The mapping description is built when the request is queued rather than read
  off g_paramSFO from the UPnP thread later.

The thread now only exists while the setting is on - turning it off makes it
remove its mappings and exit, turning it on starts one. That means __UPnPInit()
has to run after the config is actually loaded; g_Config.Init() only builds a
lookup table. QueueRequest() reconciles too, so a per-game config or a libretro
core option enabling UPnP works without a notify at every call site.

The settings checkbox is disabled in-game, since sceNet latches related
settings at boot and a game that already mapped its ports wouldn't cope with
them disappearing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
2026-08-30 00:37:05 +02:00
..
2025-07-22 17:48:20 +02:00
2025-07-22 17:48:20 +02:00
2025-07-22 17:48:20 +02:00
2026-08-11 10:28:36 +02:00
2025-08-06 17:44:54 +02:00
2026-01-18 23:40:13 +02:00
2026-08-11 09:08:52 +02:00
2025-07-20 19:30:35 +02:00
2026-08-11 20:12:10 +02:00
2026-08-11 09:08:52 +02:00
2026-08-11 20:14:01 +02:00
2025-07-20 19:32:05 +02:00
2025-02-14 09:50:55 -06:00
2026-08-11 20:14:01 +02:00
2026-08-11 10:28:36 +02:00
2026-08-11 10:28:47 +02:00
2025-01-05 19:14:15 +01:00
2026-08-11 10:28:47 +02:00
2026-08-24 09:30:44 +02:00
2025-04-03 14:13:50 +02:00
2026-08-11 20:14:01 +02:00
2026-08-12 14:02:19 +02:00
2025-02-04 11:53:22 -06:00
2026-08-08 18:18:28 +02:00