Skip to content

feat(libkernel): carry kqueue read and write events on pipes and terminals - #4910

Closed
VitzS7 wants to merge 4 commits into
boykopovar:mainfrom
VitzS7:fix/kevent-pipe-events
Closed

VitzS7 wants to merge 4 commits into
boykopovar:mainfrom
VitzS7:fix/kevent-pipe-events

Conversation

@VitzS7

@VitzS7 VitzS7 commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

What

A read or write event on a descriptor that is not a guest socket threw, in kevent and in sceKernelAddReadEvent/sceKernelAddWriteEvent alike, so a title that registers one on a pipe or on the console could not do it at all.

The kind of the descriptor now decides, following FreeBSD 11's filter sets: a socket, a pipe and a terminal carry the read and write filters (filt_soread, filt_sowrite, filt_piperead, filt_pipewrite, and tty_kqops_read_event/tty_kqops_write_event behind ttydev_kqfilter), a file and a directory only carry the vnode filter and answer EINVAL as filt_fileattach does, and so does a character device that is not a terminal, as kqueue_register answers EINVAL for a filter the descriptor has none of. The kind is GuestSockets::Kind, which asks the socket table first and then the host descriptor type — never a number range, since socket descriptors are ordinary ones — and it is kept in the knote, the way f_attach fixes the filter at registration, so a terminal revoked later still reports its end of file through the terminal filter.

The pipe filters report what filt_piperead and filt_pipewrite do: a read event carries the bytes in the pipe buffer and returns once there is any, a write event carries the free space the host exposes and returns once there is any, and an end of file is set when the other end is closed. A terminal reports the bytes of a readable canonical line, which is what ttydisc_read_poll counts through ttyinq_bytescanonicalized, and the space left in its output queue, which ttydisc_write_poll counts through ttyoutq_bytesleft and the host does not expose, so a writable terminal carries the lower bound of one byte. The low water mark (NOTE_LOWAT, or the size of the two calls) holds any of them below the mark.

Two things were needed for a wait to see a descriptor that is not a socket: the mark path and the wait now share one readiness helper, and GuestSockets::WaitAny adds a pipe descriptor to the set it polls, without which a thread blocked in kevent would never wake on it. Windows pipes are not sockets and WSAPoll cannot take them, so there the wait returns every millisecond instead.

Tested

Linux (Fedora 44, GCC 16.2.1), full build, ctest --test-dir build -E "agc_|video_out|lds_|msaa" 534/534, python3 tools/check_conventions.py --base origin/main clean.

New cases: GuestKqueue.cpp registers a read and a write event on a pipe, checks that an empty pipe is not readable, that the write event reports free space, that a read event reports the bytes in the buffer, that a NOTE_LOWAT mark holds it below the mark and that the last writer closing sets EV_EOF; it does the same for a terminal over a host pty, where a partial line is not readable, a complete one reports its bytes, the terminal going away sets EV_EOF, and a device with no filters of its own answers EINVAL; a file, a directory and a closed descriptor answer EINVAL and EBADF, replacing the assertions that a pipe threw. GuestSocketEvents.cpp does the same through sceKernelAddReadEvent's size on a pipe and on a terminal, with the wait blocked on its own equeue and a thread writing, so a wait that does not see them fails instead of passing silently.

Not tested on the console, and not reached at run time yet: PS5MC (PPSA99420) and XPSemu (PPSA97358) import kevent and both calls, and tools/import_audit.py on their relinked registries reports them as stubs, but each stops earlier on an absent import (ixrw0h2tWuI, chflags, and x7g7Ebeo8-U, mlockall) that this PR does not touch. The semantics follow the FreeBSD 11 sources the existing filters were written from (kern_event.c, uipc_socket.c, tty.c, sys/sys/ttyqueue.h).

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: feat(libkernel): wait for the low water mark in the kqueue descriptor events #4908 (the low water mark path this builds on)
  • Unblocks: PS5MC PPSA99420 and XPSemu PPSA97358 import kevent and these calls; neither reaches them yet
  • AI-assisted: yes

Note on coordination: #4391 gives guest sockets ordinary descriptors and rewrites the socket checks of this file to socket-table membership. This PR takes the membership check with it (in DescriptorFilterError and by asking the socket table before the host descriptor type), so the two do not have to be merged in a particular order.

@VitzS7 VitzS7 changed the title feat(libkernel): carry kqueue read and write events on pipes feat(libkernel): carry kqueue read and write events on pipes and terminals Oct 13, 2026
@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Progress report (dacd785 → 6f51e1d)

📈 System libraries: 88.21% (+0.06%, +2 implemented functions)

✅ 2 implemented
Library Name
libkernel sceKernelAddReadEvent
libkernel sceKernelAddWriteEvent

@xXPBSXx

xXPBSXx commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

Windows test: head ca734ca92 merged onto current main 88107b7de (clean merge, no conflicts). Windows 10, MinGW GCC (WinLibs), Ninja Release, -j2: full build 0 errors; ctest with the filter guest_kqueue|guest_socket_event: guest_kqueue passed, guest_socket_events passed (2 test(s), 0 failed); python tools/check_conventions.py --base upstream/main passed. I did not run the new tests against main without the change.

AI-assisted (Claude Code).

@VitzS7

VitzS7 commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks for the Windows run — that is the one platform I cannot check here, so it closes half of the checklist.

The other half, which your note says you did not run: the cases this PR adds fail on main without it. On main a read or write event on a pipe or a terminal throws, and sceKernelAddReadEvent and sceKernelAddWriteEvent throw for either, so guest_kqueue aborts at the pipe case (it asserted that throw before this PR) and guest_socket_events at the size case, rather than passing silently.

Coordination with #4924 and #4933

The overlap note points at the same lines of Equeue.cpp, and there is a semantic side to it, since both PRs make libSceNet sockets ordinary descriptors as well. GuestSockets::Kind, which this PR introduces, answers EINVAL for a descriptor that is neither a libkernel socket, a pipe, a terminal, a file nor a directory. A libSceNet socket host descriptor is a socket, but it is not in the libkernel table, so today it would be classified as that last kind and a read or write event on it would answer EINVAL where it should register a knote.

Once #4924 or #4933 lands, the native lookup they register with libkernel is the natural source for that answer, and I am happy to rebase this on whichever lands first and ask it there instead of deriving the kind from the host descriptor alone.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 13, 2026
… events

Before: NOTE_LOWAT and a size above one in sceKernelAddReadEvent and
sceKernelAddWriteEvent threw, so a title waiting for a number of bytes could not
register the event at all (PS5MC and XPSemu import both the kevent filter and
the two calls). Now the knote holds the event until the socket has the requested
bytes, or the requested free space, as FreeBSD's filt_soread and filt_sowrite
do before they compare kn_data against the mark; an end of file is still
reported below it. A kevent that re-adds the knote without NOTE_LOWAT clears the
watermark, as kqueue_register copies the whole kevent over it.

A mark is not something poll readiness can express: a socket below the mark is
already readable, so a wait that has one returns to its caller every millisecond
instead of spinning on the readable socket, the slice the pipe path already uses.
Before: a read or write event on a descriptor that is not a guest socket threw,
so a title that registers one on a pipe could not do it at all, and the event a
file answers there was a throw too. Now the kind of the descriptor decides, as
FreeBSD's filter sets do: a socket and a pipe carry the read and write filters,
a file and a directory only carry the vnode one and answer EINVAL as
filt_fileattach does, and a descriptor that is none of those throws, because the
terminal filters of tty_kqops_read_event and tty_kqops_write_event are not
modelled.

The filters follow filt_piperead and filt_pipewrite: a read event reports the
bytes in the pipe buffer and returns once there is any, a write event reports
the free space the host exposes and returns once there is any, an end of file is
set when the other end is closed, and the low water mark of a kevent change or
of sceKernelAddReadEvent and sceKernelAddWriteEvent holds the event below it as
it does for a socket.

A socket is recognised by socket-table membership rather than by a descriptor
number, since those are ordinary ones, and the wait adds a pipe descriptor to
the set it polls, without which a blocked kevent would never see the event. A
pipe is not pollable on Windows, so there the wait slices at a millisecond.
A descriptor that is neither a socket, a pipe nor a file still threw a read or
write event, which left sceKernelAddReadEvent and sceKernelAddWriteEvent with an
unimplemented path. A terminal is what was missing: ttydev_kqfilter attaches the
read and write filters to it, so tty_kqops_read_event and tty_kqops_write_event
apply, with the number ttydisc_read_poll reports, the readable lines a canonical
terminal has, and that of ttydisc_write_poll, the space left in the output queue,
which the host does not expose, so a writable terminal reports the lower bound of
one byte. A character device that is not a terminal, and a file or a directory,
answer EINVAL, as kqueue_register does for a filter the descriptor has none of.

The kind a knote was attached to is kept in it, the way f_attach fixes the filter
at registration: a terminal revoked later still reports its end of file through
the terminal filter instead of being read as a device with no filter, which is
what asking the host again would report.
@VitzS7
VitzS7 force-pushed the fix/kevent-pipe-events branch from ca734ca to 6f51e1d Compare October 13, 2026 19:48
@VitzS7

VitzS7 commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

Rebased on current main (dacd78525), which now carries #4391; Depends on: #4908 still holds, since this branch sits on its two commits.

Four files conflicted (Equeue.cpp, Kqueue.cpp, SocketRuntime.hpp, Udp.cpp). The resolution keeps your socket-table membership work — DescriptorFilterError and the isSocket checks stay as you wrote them — and puts the descriptor kind on top: GuestSockets::Kind asks the socket table first and the host descriptor type second, and AddDescriptorKnote/AddDescriptorEvent use it in place of the non-socket throw, so a pipe or a terminal registers a knote while a file answers EINVAL, exactly as the commit messages describe.

One thing the rebuild caught, worth mentioning because it is the kind of thing a rebase hides: GuestSockets::Poll (#4391) and GuestSockets::Kind (this PR) landed at the same place in Udp.cpp, and my first union of the two lost the brace that closes Poll, which the compiler then read as everything after it being nested inside that function. It is fixed inside the pipes commit, so the commit builds on its own.

Local verification on dacd78525 + this branch (cmake → python3 tools/check_test_registration.py build → cmake --build build → cmake --build build --target libs): ctest --test-dir build -E "agc_|video_out|lds_|msaa" 570/570, guest_kqueue, guest_socket_events and guest_equeue included, and tools/check_conventions.py --base origin/main clean.

@CnRJay CnRJay left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filter selection by descriptor kind follows FreeBSD's filter sets and looks right. One Windows timing point this extends to kevent: a pipe in the set makes GuestSockets::WaitAny loop with a 1 ms slice (WSAPoll(..., slice) when sockets are present, std::this_thread::sleep_for(1ms) otherwise). Both follow the coarse Windows timer, so each slice usually takes about 15.6 ms rather than 1 ms (#1471). That's why #4980 and #4754 move the poll and select sleeps to PreciseSleepUs.

For kevent that matters more than for poll. A thread parked in kevent on a pipe (an event loop's wake-up pipe, a log pipe) now sees each write up to ~15 ms late. With NOTE_LOWAT from #4908 on top, the wait rechecks at the same coarse rate. Using PreciseSleepUs(1000) for the pipe-only slice, and a high-resolution wait for the mixed case (for example a pipe-ready event, or splitting the socket wait into a 0-timeout WSAPoll plus PreciseSleepUs), would make "every millisecond" actually 1 ms. Since it's the shared loop, it could also be a separate small PR that both this and #4908 depend on.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 14, 2026
@oneandonlydean

Copy link
Copy Markdown
Collaborator

Closing: no title is known to reach this. With the queue at 480+ open PRs, please hold off on new PRs that don't name a title needing the change until it's down. If a title stops on this on current main, reopen with the title ID and the log line.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs libkernel needs rebase Conflicts with main, rebase needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants