Make SO_POLL complete asynchronously
This commit is contained in:
parent
a1a107dc08
commit
b68ab560dc
|
@ -50,13 +50,6 @@
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// WSAPoll doesn't support POLLPRI and POLLWRBAND flags
|
|
||||||
#ifdef _WIN32
|
|
||||||
#define UNSUPPORTED_WSAPOLL POLLPRI | POLLWRBAND
|
|
||||||
#else
|
|
||||||
#define UNSUPPORTED_WSAPOLL 0
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace IOS::HLE::Device
|
namespace IOS::HLE::Device
|
||||||
{
|
{
|
||||||
enum SOResultCode : s32
|
enum SOResultCode : s32
|
||||||
|
@ -80,6 +73,12 @@ NetIPTop::~NetIPTop()
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NetIPTop::DoState(PointerWrap& p)
|
||||||
|
{
|
||||||
|
DoStateShared(p);
|
||||||
|
WiiSockMan::GetInstance().DoState(p);
|
||||||
|
}
|
||||||
|
|
||||||
static constexpr u32 inet_addr(u8 a, u8 b, u8 c, u8 d)
|
static constexpr u32 inet_addr(u8 a, u8 b, u8 c, u8 d)
|
||||||
{
|
{
|
||||||
return (static_cast<u32>(a) << 24) | (static_cast<u32>(b) << 16) | (static_cast<u32>(c) << 8) | d;
|
return (static_cast<u32>(a) << 24) | (static_cast<u32>(b) << 16) | (static_cast<u32>(c) << 8) | d;
|
||||||
|
@ -599,77 +598,45 @@ IPCCommandResult NetIPTop::HandleInetNToPRequest(const IOCtlRequest& request)
|
||||||
|
|
||||||
IPCCommandResult NetIPTop::HandlePollRequest(const IOCtlRequest& request)
|
IPCCommandResult NetIPTop::HandlePollRequest(const IOCtlRequest& request)
|
||||||
{
|
{
|
||||||
// Map Wii/native poll events types
|
WiiSockMan& sm = WiiSockMan::GetInstance();
|
||||||
struct
|
|
||||||
|
if (!request.buffer_in || !request.buffer_out)
|
||||||
|
return GetDefaultReply(-SO_EINVAL);
|
||||||
|
|
||||||
|
// Negative timeout indicates wait forever
|
||||||
|
const s64 timeout = static_cast<s64>(Memory::Read_U64(request.buffer_in));
|
||||||
|
|
||||||
|
const u32 nfds = request.buffer_out_size / 0xc;
|
||||||
|
if (nfds == 0 || nfds > WII_SOCKET_FD_MAX)
|
||||||
{
|
{
|
||||||
int native;
|
ERROR_LOG(IOS_NET, "IOCTL_SO_POLL failed: Invalid array size %d, ret=%d", nfds, -SO_EINVAL);
|
||||||
int wii;
|
return GetDefaultReply(-SO_EINVAL);
|
||||||
} mapping[] = {
|
}
|
||||||
{POLLRDNORM, 0x0001}, {POLLRDBAND, 0x0002}, {POLLPRI, 0x0004}, {POLLWRNORM, 0x0008},
|
|
||||||
{POLLWRBAND, 0x0010}, {POLLERR, 0x0020}, {POLLHUP, 0x0040}, {POLLNVAL, 0x0080},
|
|
||||||
};
|
|
||||||
|
|
||||||
u32 unknown = Memory::Read_U32(request.buffer_in);
|
|
||||||
u32 timeout = Memory::Read_U32(request.buffer_in + 4);
|
|
||||||
|
|
||||||
int nfds = request.buffer_out_size / 0xc;
|
|
||||||
if (nfds == 0)
|
|
||||||
ERROR_LOG(IOS_NET, "Hidden POLL");
|
|
||||||
|
|
||||||
std::vector<pollfd_t> ufds(nfds);
|
std::vector<pollfd_t> ufds(nfds);
|
||||||
|
|
||||||
for (int i = 0; i < nfds; ++i)
|
for (u32 i = 0; i < nfds; ++i)
|
||||||
{
|
{
|
||||||
s32 wii_fd = Memory::Read_U32(request.buffer_out + 0xc * i);
|
const s32 wii_fd = Memory::Read_U32(request.buffer_out + 0xc * i);
|
||||||
ufds[i].fd = WiiSockMan::GetInstance().GetHostSocket(wii_fd); // fd
|
ufds[i].fd = sm.GetHostSocket(wii_fd); // fd
|
||||||
int events = Memory::Read_U32(request.buffer_out + 0xc * i + 4); // events
|
const int events = Memory::Read_U32(request.buffer_out + 0xc * i + 4); // events
|
||||||
ufds[i].revents = Memory::Read_U32(request.buffer_out + 0xc * i + 8); // revents
|
ufds[i].revents = 0;
|
||||||
|
|
||||||
// Translate Wii to native events
|
// Translate Wii to native events
|
||||||
int unhandled_events = events;
|
ufds[i].events = WiiSockMan::ConvertEvents(events, WiiSockMan::ConvertDirection::WiiToNative);
|
||||||
ufds[i].events = 0;
|
|
||||||
for (auto& map : mapping)
|
|
||||||
{
|
|
||||||
if (events & map.wii)
|
|
||||||
ufds[i].events |= map.native;
|
|
||||||
unhandled_events &= ~map.wii;
|
|
||||||
}
|
|
||||||
DEBUG_LOG(IOS_NET,
|
DEBUG_LOG(IOS_NET,
|
||||||
"IOCTL_SO_POLL(%d) "
|
"IOCTL_SO_POLL(%d) "
|
||||||
"Sock: %08x, Unknown: %08x, Events: %08x, "
|
"Sock: %08x, Events: %08x, "
|
||||||
"NativeEvents: %08x",
|
"NativeEvents: %08x",
|
||||||
i, wii_fd, unknown, events, ufds[i].events);
|
i, wii_fd, events, ufds[i].events);
|
||||||
|
|
||||||
// Do not pass return-only events to the native poll
|
// Do not pass return-only events to the native poll
|
||||||
ufds[i].events &= ~(POLLERR | POLLHUP | POLLNVAL | UNSUPPORTED_WSAPOLL);
|
ufds[i].events &= ~(POLLERR | POLLHUP | POLLNVAL | UNSUPPORTED_WSAPOLL);
|
||||||
|
|
||||||
if (unhandled_events)
|
|
||||||
ERROR_LOG(IOS_NET, "SO_POLL: unhandled Wii event types: %04x", unhandled_events);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int ret = poll(ufds.data(), nfds, timeout);
|
// Prevents blocking emulation on a blocking poll
|
||||||
ret = WiiSockMan::GetNetErrorCode(ret, "SO_POLL", false);
|
sm.AddPollCommand({request.address, request.buffer_out, std::move(ufds), timeout});
|
||||||
|
return GetNoReply();
|
||||||
for (int i = 0; i < nfds; ++i)
|
|
||||||
{
|
|
||||||
// Translate native to Wii events
|
|
||||||
int revents = 0;
|
|
||||||
for (auto& map : mapping)
|
|
||||||
{
|
|
||||||
if (ufds[i].revents & map.native)
|
|
||||||
revents |= map.wii;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No need to change fd or events as they are input only.
|
|
||||||
// Memory::Write_U32(ufds[i].fd, request.buffer_out + 0xc*i); //fd
|
|
||||||
// Memory::Write_U32(events, request.buffer_out + 0xc*i + 4); //events
|
|
||||||
Memory::Write_U32(revents, request.buffer_out + 0xc * i + 8); // revents
|
|
||||||
|
|
||||||
DEBUG_LOG(IOS_NET, "IOCTL_SO_POLL socket %d wevents %08X events %08X revents %08X", i, revents,
|
|
||||||
ufds[i].events, ufds[i].revents);
|
|
||||||
}
|
|
||||||
|
|
||||||
return GetDefaultReply(ret);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IPCCommandResult NetIPTop::HandleGetHostByNameRequest(const IOCtlRequest& request)
|
IPCCommandResult NetIPTop::HandleGetHostByNameRequest(const IOCtlRequest& request)
|
||||||
|
|
|
@ -13,6 +13,13 @@
|
||||||
#include <ws2tcpip.h>
|
#include <ws2tcpip.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// WSAPoll doesn't support POLLPRI and POLLWRBAND flags
|
||||||
|
#ifdef _WIN32
|
||||||
|
constexpr int UNSUPPORTED_WSAPOLL = POLLPRI | POLLWRBAND;
|
||||||
|
#else
|
||||||
|
constexpr int UNSUPPORTED_WSAPOLL = 0;
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace IOS::HLE
|
namespace IOS::HLE
|
||||||
{
|
{
|
||||||
enum NET_IOCTL
|
enum NET_IOCTL
|
||||||
|
@ -62,6 +69,7 @@ public:
|
||||||
NetIPTop(Kernel& ios, const std::string& device_name);
|
NetIPTop(Kernel& ios, const std::string& device_name);
|
||||||
virtual ~NetIPTop();
|
virtual ~NetIPTop();
|
||||||
|
|
||||||
|
void DoState(PointerWrap& p) override;
|
||||||
IPCCommandResult IOCtl(const IOCtlRequest& request) override;
|
IPCCommandResult IOCtl(const IOCtlRequest& request) override;
|
||||||
IPCCommandResult IOCtlV(const IOCtlVRequest& request) override;
|
IPCCommandResult IOCtlV(const IOCtlVRequest& request) override;
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
#include "Core/IOS/Network/Socket.h"
|
#include "Core/IOS/Network/Socket.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <numeric>
|
||||||
|
|
||||||
#include <mbedtls/error.h>
|
#include <mbedtls/error.h>
|
||||||
#ifndef _WIN32
|
#ifndef _WIN32
|
||||||
#include <arpa/inet.h>
|
#include <arpa/inet.h>
|
||||||
|
@ -697,7 +699,7 @@ void WiiSockMan::Update()
|
||||||
|
|
||||||
while (socket_iter != end_socks)
|
while (socket_iter != end_socks)
|
||||||
{
|
{
|
||||||
WiiSocket& sock = socket_iter->second;
|
const WiiSocket& sock = socket_iter->second;
|
||||||
if (sock.IsValid())
|
if (sock.IsValid())
|
||||||
{
|
{
|
||||||
FD_SET(sock.fd, &read_fds);
|
FD_SET(sock.fd, &read_fds);
|
||||||
|
@ -712,7 +714,8 @@ void WiiSockMan::Update()
|
||||||
socket_iter = WiiSockets.erase(socket_iter);
|
socket_iter = WiiSockets.erase(socket_iter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s32 ret = select(nfds, &read_fds, &write_fds, &except_fds, &t);
|
|
||||||
|
const s32 ret = select(nfds, &read_fds, &write_fds, &except_fds, &t);
|
||||||
|
|
||||||
if (ret >= 0)
|
if (ret >= 0)
|
||||||
{
|
{
|
||||||
|
@ -730,6 +733,87 @@ void WiiSockMan::Update()
|
||||||
elem.second.Update(false, false, false);
|
elem.second.Update(false, false, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
UpdatePollCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WiiSockMan::UpdatePollCommands()
|
||||||
|
{
|
||||||
|
static constexpr int error_event = (POLLHUP | POLLERR);
|
||||||
|
|
||||||
|
if (pending_polls.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto now = std::chrono::high_resolution_clock::now();
|
||||||
|
const auto elapsed_d = std::chrono::duration_cast<std::chrono::milliseconds>(now - last_time);
|
||||||
|
const auto elapsed = elapsed_d.count();
|
||||||
|
last_time = now;
|
||||||
|
|
||||||
|
for (auto& pcmd : pending_polls)
|
||||||
|
{
|
||||||
|
// Don't touch negative timeouts
|
||||||
|
if (pcmd.timeout > 0)
|
||||||
|
pcmd.timeout = std::max<s64>(0, pcmd.timeout - elapsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
pending_polls.erase(
|
||||||
|
std::remove_if(
|
||||||
|
pending_polls.begin(), pending_polls.end(),
|
||||||
|
[this](auto& pcmd) {
|
||||||
|
const auto request = Request(pcmd.request_addr);
|
||||||
|
auto& pfds = pcmd.wii_fds;
|
||||||
|
int ret = 0;
|
||||||
|
|
||||||
|
// Happens only on savestate load
|
||||||
|
if (pfds[0].revents & error_event)
|
||||||
|
{
|
||||||
|
ret = static_cast<int>(pfds.size());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Make the behavior of poll consistent across platforms by not passing:
|
||||||
|
// - Set with invalid fds, revents is set to 0 (Linux) or POLLNVAL (Windows)
|
||||||
|
// - Set without a valid socket, raises an error on Windows
|
||||||
|
std::vector<int> original_order(pfds.size());
|
||||||
|
std::iota(original_order.begin(), original_order.end(), 0);
|
||||||
|
// Select indices with valid fds
|
||||||
|
auto mid = std::partition(original_order.begin(), original_order.end(), [&](auto i) {
|
||||||
|
return GetHostSocket(Memory::Read_U32(pcmd.buffer_out + 0xc * i)) >= 0;
|
||||||
|
});
|
||||||
|
const auto n_valid = std::distance(original_order.begin(), mid);
|
||||||
|
|
||||||
|
// Move all the valid pollfds to the front of the vector
|
||||||
|
for (auto i = 0; i < n_valid; ++i)
|
||||||
|
std::swap(pfds[i], pfds[original_order[i]]);
|
||||||
|
|
||||||
|
if (n_valid > 0)
|
||||||
|
ret = poll(pfds.data(), n_valid, 0);
|
||||||
|
if (ret < 0)
|
||||||
|
ret = GetNetErrorCode(ret, "UpdatePollCommands", false);
|
||||||
|
|
||||||
|
// Move everything back to where they were
|
||||||
|
for (auto i = 0; i < n_valid; ++i)
|
||||||
|
std::swap(pfds[i], pfds[original_order[i]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ret == 0 && pcmd.timeout)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Translate native to Wii events,
|
||||||
|
for (u32 i = 0; i < pfds.size(); ++i)
|
||||||
|
{
|
||||||
|
const int revents = ConvertEvents(pfds[i].revents, ConvertDirection::NativeToWii);
|
||||||
|
|
||||||
|
// No need to change fd or events as they are input only.
|
||||||
|
// Memory::Write_U32(ufds[i].fd, request.buffer_out + 0xc*i); //fd
|
||||||
|
// Memory::Write_U32(events, request.buffer_out + 0xc*i + 4); //events
|
||||||
|
Memory::Write_U32(revents, pcmd.buffer_out + 0xc * i + 8); // revents
|
||||||
|
DEBUG_LOG(IOS_NET, "IOCTL_SO_POLL socket %d wevents %08X events %08X revents %08X", i,
|
||||||
|
revents, pfds[i].events, pfds[i].revents);
|
||||||
|
}
|
||||||
|
GetIOS()->EnqueueIPCReply(request, ret);
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
pending_polls.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
void WiiSockMan::Convert(WiiSockAddrIn const& from, sockaddr_in& to)
|
void WiiSockMan::Convert(WiiSockAddrIn const& from, sockaddr_in& to)
|
||||||
|
@ -739,6 +823,43 @@ void WiiSockMan::Convert(WiiSockAddrIn const& from, sockaddr_in& to)
|
||||||
to.sin_port = from.port;
|
to.sin_port = from.port;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s32 WiiSockMan::ConvertEvents(s32 events, ConvertDirection dir)
|
||||||
|
{
|
||||||
|
constexpr struct
|
||||||
|
{
|
||||||
|
int native;
|
||||||
|
int wii;
|
||||||
|
} mapping[] = {
|
||||||
|
{POLLRDNORM, 0x0001}, {POLLRDBAND, 0x0002}, {POLLPRI, 0x0004}, {POLLWRNORM, 0x0008},
|
||||||
|
{POLLWRBAND, 0x0010}, {POLLERR, 0x0020}, {POLLHUP, 0x0040}, {POLLNVAL, 0x0080},
|
||||||
|
};
|
||||||
|
|
||||||
|
s32 converted_events = 0;
|
||||||
|
s32 unhandled_events = 0;
|
||||||
|
|
||||||
|
if (dir == ConvertDirection::NativeToWii)
|
||||||
|
{
|
||||||
|
for (const auto& map : mapping)
|
||||||
|
{
|
||||||
|
if (events & map.native)
|
||||||
|
converted_events |= map.wii;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
unhandled_events = events;
|
||||||
|
for (const auto& map : mapping)
|
||||||
|
{
|
||||||
|
if (events & map.wii)
|
||||||
|
converted_events |= map.native;
|
||||||
|
unhandled_events &= ~map.wii;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (unhandled_events)
|
||||||
|
ERROR_LOG(IOS_NET, "SO_POLL: unhandled Wii event types: %04x", unhandled_events);
|
||||||
|
return converted_events;
|
||||||
|
}
|
||||||
|
|
||||||
void WiiSockMan::Convert(sockaddr_in const& from, WiiSockAddrIn& to, s32 addrlen)
|
void WiiSockMan::Convert(sockaddr_in const& from, WiiSockAddrIn& to, s32 addrlen)
|
||||||
{
|
{
|
||||||
to.addr.addr = from.sin_addr.s_addr;
|
to.addr.addr = from.sin_addr.s_addr;
|
||||||
|
@ -750,6 +871,35 @@ void WiiSockMan::Convert(sockaddr_in const& from, WiiSockAddrIn& to, s32 addrlen
|
||||||
to.len = addrlen;
|
to.len = addrlen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WiiSockMan::DoState(PointerWrap& p)
|
||||||
|
{
|
||||||
|
bool saving =
|
||||||
|
p.mode == PointerWrap::Mode::MODE_WRITE || p.mode == PointerWrap::Mode::MODE_MEASURE;
|
||||||
|
auto size = pending_polls.size();
|
||||||
|
p.Do(size);
|
||||||
|
if (!saving)
|
||||||
|
pending_polls.resize(size);
|
||||||
|
for (auto& pcmd : pending_polls)
|
||||||
|
{
|
||||||
|
p.Do(pcmd.request_addr);
|
||||||
|
p.Do(pcmd.buffer_out);
|
||||||
|
p.Do(pcmd.wii_fds);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saving)
|
||||||
|
return;
|
||||||
|
for (auto& pcmd : pending_polls)
|
||||||
|
{
|
||||||
|
for (auto& wfd : pcmd.wii_fds)
|
||||||
|
wfd.revents = (POLLHUP | POLLERR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WiiSockMan::AddPollCommand(const PollCommand& cmd)
|
||||||
|
{
|
||||||
|
pending_polls.push_back(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
void WiiSockMan::UpdateWantDeterminism(bool want)
|
void WiiSockMan::UpdateWantDeterminism(bool want)
|
||||||
{
|
{
|
||||||
// If we switched into movie recording, kill existing sockets.
|
// If we switched into movie recording, kill existing sockets.
|
||||||
|
|
|
@ -212,6 +212,20 @@ private:
|
||||||
class WiiSockMan
|
class WiiSockMan
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
enum class ConvertDirection
|
||||||
|
{
|
||||||
|
WiiToNative,
|
||||||
|
NativeToWii
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PollCommand
|
||||||
|
{
|
||||||
|
u32 request_addr;
|
||||||
|
u32 buffer_out;
|
||||||
|
std::vector<pollfd_t> wii_fds;
|
||||||
|
s64 timeout;
|
||||||
|
};
|
||||||
|
|
||||||
static s32 GetNetErrorCode(s32 ret, const char* caller, bool isRW);
|
static s32 GetNetErrorCode(s32 ret, const char* caller, bool isRW);
|
||||||
static char* DecodeError(s32 ErrorCode);
|
static char* DecodeError(s32 ErrorCode);
|
||||||
|
|
||||||
|
@ -223,6 +237,10 @@ public:
|
||||||
void Update();
|
void Update();
|
||||||
static void Convert(WiiSockAddrIn const& from, sockaddr_in& to);
|
static void Convert(WiiSockAddrIn const& from, sockaddr_in& to);
|
||||||
static void Convert(sockaddr_in const& from, WiiSockAddrIn& to, s32 addrlen = -1);
|
static void Convert(sockaddr_in const& from, WiiSockAddrIn& to, s32 addrlen = -1);
|
||||||
|
static s32 ConvertEvents(s32 events, ConvertDirection dir);
|
||||||
|
|
||||||
|
void DoState(PointerWrap& p);
|
||||||
|
void AddPollCommand(const PollCommand& cmd);
|
||||||
// NON-BLOCKING FUNCTIONS
|
// NON-BLOCKING FUNCTIONS
|
||||||
s32 NewSocket(s32 af, s32 type, s32 protocol);
|
s32 NewSocket(s32 af, s32 type, s32 protocol);
|
||||||
s32 AddSocket(s32 fd, bool is_rw);
|
s32 AddSocket(s32 fd, bool is_rw);
|
||||||
|
@ -256,7 +274,12 @@ private:
|
||||||
WiiSockMan(WiiSockMan&&) = delete;
|
WiiSockMan(WiiSockMan&&) = delete;
|
||||||
WiiSockMan& operator=(WiiSockMan&&) = delete;
|
WiiSockMan& operator=(WiiSockMan&&) = delete;
|
||||||
|
|
||||||
|
void UpdatePollCommands();
|
||||||
|
|
||||||
std::unordered_map<s32, WiiSocket> WiiSockets;
|
std::unordered_map<s32, WiiSocket> WiiSockets;
|
||||||
s32 errno_last;
|
s32 errno_last;
|
||||||
|
std::vector<PollCommand> pending_polls;
|
||||||
|
std::chrono::time_point<std::chrono::high_resolution_clock> last_time =
|
||||||
|
std::chrono::high_resolution_clock::now();
|
||||||
};
|
};
|
||||||
} // namespace IOS::HLE
|
} // namespace IOS::HLE
|
||||||
|
|
|
@ -74,7 +74,7 @@ static Common::Event g_compressAndDumpStateSyncEvent;
|
||||||
static std::thread g_save_thread;
|
static std::thread g_save_thread;
|
||||||
|
|
||||||
// Don't forget to increase this after doing changes on the savestate system
|
// Don't forget to increase this after doing changes on the savestate system
|
||||||
constexpr u32 STATE_VERSION = 117; // Last changed in PR 7396
|
constexpr u32 STATE_VERSION = 118; // Last changed in PR 8813
|
||||||
|
|
||||||
// Maps savestate versions to Dolphin versions.
|
// Maps savestate versions to Dolphin versions.
|
||||||
// Versions after 42 don't need to be added to this list,
|
// Versions after 42 don't need to be added to this list,
|
||||||
|
|
Loading…
Reference in New Issue