Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Win32 interactive resizing proof-of-concept #1296

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

AnyOldName3
Copy link
Contributor

Basically works by setting up a callback to draw a new frame which we do when the WM_PAINT event arrives, which is how Windows tries to tell us it's time to redraw the window due to a resize etc.

Unlike the WM_SIZE event, it only happens when we've consumed the whole message queue, so it should be pretty similar to the regular situation where a frame happens after pollEvents is called.

This approach is necessary in the first place because Win32 window messages can be reentrant, so the handler for one event, like a click on the corner of a window, can in turn fire a series of extra messages for the drag and resizes and release, without first returning.

This PR seems to work fine on Windows and shouldn't have changed anything on other platforms, but I've not tested it on other platforms, and it lacks some polish as I wasn't sure how best to accomplish the advanceToNextFrame splitting, so thought I'd wait for your opinion before doing more than the minimum.

Basically works by setting up a callback to draw a new frame which we do when the WM_PAINT event arrives, which is how Windows tries to tell us it's time to redraw the window due to a resize etc.

Unlike the WM_SIZE event, it only happens when we've consumed the whole message queue, so it should be pretty similar to the regular situation where a frame happens after `pollEvents` is called.

This approach is necessary in the first place because Win32 window messages can be reentrant, so the handler for one event, like a click on the corner of a window, can in turn fire a series of extra messages for the drag and resizes and release, without first returning.
@robertosfield
Copy link
Collaborator

Done a quick review and it really feels like an obscure non local solution for a platform specific problem. I can't imagine many engineers would be able to see the code and understand what it's doing and why.

We could put lots of comments in the code to explain why this hacky code exists but far better would be to come up with a localized solution to the deal with the oddities of Windows API behavior rather than pollute the wider code with hacks.

@AnyOldName3
Copy link
Contributor Author

The 'proper' solution is to draw a new frame when we get a WM_PAINT message, and with the frame loop driving the event loop instead of the other way around, a we need an extra frame callback is the least hacky option. The only non-hacky option would be to have the event loop drive the frame loop, but I imagine that'd need a lot of stuff to be rearranged.

The other two potential hacks would be running the message pump on another thread (which would mean creating the window on another thread and synchronising a lot of stuff, which I'd expect to end up much more hacky) or providing our own handlers for the recursive events which aren't recursive (which probably wouldn't match native look and feel unless we lifted code from WINE/Proton/ReactOS, but they're GPL, so we can't).

I could have a go at some alternative implementations and then it'd be easier to judge which is least bad.

@robertosfield
Copy link
Collaborator

I'm tidying up cppcheck issues today in prep for making a 1.1.8 dev release. I will pop over into Windows and see what I can learn about this resize issue. Don't worry about coming up with alternatives, once I properly understand the issue I should be in a better place to review your existing changes and perhaps spot a cleaner way to navigate through this.

@robertosfield
Copy link
Collaborator

Unfortunately I've been drowned in cppcheck issues fixing today so have only briefly been able to look at the resize issues. I'll need to return this when I get back from my trip.

To test I ran vsgviewer models/openstreetmap.vsgt and recording an animation path then ran vsgviewer with this animation path so that it's clear when the viewer is rendering frames and when it's not. If I don't resize the animation plays correctly but as soon as I start to resize the window the frames stop rendering and the view in the window just gets resized by Windows.

This is clear that something in the Windows API is stopping the usual frame rendering from happening during resize. I don't know where it's getting stuck, and it's possible this has already been explained in the various threads, but this looks to be crux of the issue. Having callbacks might be a workaround for Windows stopping the viewer from rendering as it should be doing, but it's hacky so I'd rather not go polluting other parts of the VSG to workaround to this Win32 pecularity.

@AnyOldName3
Copy link
Contributor Author

I wouldn't really call it a Win32 peculiarity. It's the only surviving mainstream native desktop windowing API that uses the approach, but there's only Win32, X.org, Wayland and the MacOS one (it's either Quartz or Core Graphics but it's unclear which) left, so it's hardly an odd duck in a sea of uniformity. Plenty of non-native frameworks (e.g. Qt, which we've already discussed) use the same approach as Win32 where an event handler will dispatch child events without returning, and may not return until an input action's entirely finished. It's just VSG being set up under the assumption that that'll never happen that causes the problem.

@robertosfield
Copy link
Collaborator

robertosfield commented Oct 8, 2024 via email

@AnyOldName3
Copy link
Contributor Author

That's just a matter of no one reporting/noticing it. Windows has worked this way since before X.org, Wayland, and whatever-MacOS'-current-API-is-called were invented. The Win32 API doesn't change behaviour, hence binaries from before I was born still running fine on modern versions of Windows, and people justifiably claiming that Win32 Is The Only Stable ABI on Linux.

@robertosfield
Copy link
Collaborator

You might want to look up the dates of when X11 and Apple started with graphical user interfaces... X11 was released 8 years before Win32. I was developing X11 and Motif applications in 92, 3 years before Windows 95 was released and MS finally got into the 32 bit computing era.

@AnyOldName3
Copy link
Contributor Author

I said Windows worked this way since then, not the Win32 API. This predated Win32, which was largely backwards-compatible with Win16, and a subset of Win32 was already usable on Windows 3.1 in 1993 (despite being a 16-bit OS, applications could run in 32-bit mode), or what was at the time the whole thing on Windows NT 3.1 the same year.

If you look at an example for Windows 1.0 that'd build and run in 1985, it's got some major similarities to the current Win32_Window.cpp in the VSG: https://github.com/TransmissionZero/Windows-1-Example-Application/blob/master/MainWnd.c because the event loop implementation hasn't changed in the 39 years since then.

@robertosfield
Copy link
Collaborator

I was writing event driven applications before Win32 existed on X11/Motif/OpenGL, it's not anything new.

Event driven applications are perfect fine for interactive applications. They aren't appropriate for real-time applications where the frame rate needs to be kept at a constant rate and never interrupted by events, the OSG/VSG heart land is vis-sim so out of the box it's frame driven.

Sure you can do even driven just fine with the OSG/VSG as well, but that's for interactive applications nor real-time ones, for the later we'll always need to be frame driven.

@AnyOldName3
Copy link
Contributor Author

Even if things are event-driven, you can still make things real-time by having a budget (either time or counter) to cap the number of events you'll handle before ignoring any more and drawing a frame anyway. That's more-or-less orthogonal to whether things are event-driven or not, as not having a budget when consuming incoming events as part of a frame loop, like VSG does now, means a flood of events can cause a missed frame deadline. There's a counter in the XCB windows's pollEvents implementation that gets incremented for each event, but never used, so maybe you put that there to try and introduce a per-frame limit that just never got finished.

Anyway, that's not really particularly relevant to the root cause of the problem. The real cause is that event handlers are free to remain on the stack and emit their own child events as long as they run their own event loop to take over from the default one. For example, a mouse button down handler might look something like:

void onMouseButtonDown(event)
{
  onDragStart();
  while (innerEvent = blockUntilEvent(); innerEvent.type != mouseButtonUp)
  {
    handleEvent(innerEvent);
    if (innerEvent.type == mouseMove)
      onDrag();
  }
  onDragEnd();
}

If an event handler might not return until some other event arrives later, then you need some way to still draw frames before it returns, whether that's doing it on another thread, or drawing in an event handler (potentially after also adding an event source based on a timer so you don't skip frames when no events are generated for a while).

Some of Windows' default message handlers and Qt's event handlers are set up like this. It doesn't cause problems with vsgQt as frames are drawn in response to a timer event and don't care what's already on the stack, whereas it does cause problems with Win32_Window because it can't draw frames until all event handlers have returned.

@robertosfield
Copy link
Collaborator

robertosfield commented Oct 10, 2024

Head in hands, no you can't make real-time graphics application that are event driven, professional simulators just don't and shouldn't work that way.

Sure write games that don't really care whether a frame drops or comes in early, or interactive applications that are event driven, but please keep the event driven far away from real-time graphics simulators and VR, even fudging things doesn't cut it. The OSG and VSG strive to be professional grade APIs that can do real-time graphics and VR from the ground up.

This is a pretty fundamental technical point, trying to push event driven as OK to someone with years more experience and knowledge of the topic is really stupid, your ego has got ahead of your understanding.

I appreciate your input on trying to get this problem resolved but please stick to what you know, not what you think you know.

@AnyOldName3
Copy link
Contributor Author

I'm wondering if maybe we're using different definitions of real-time (in which case, I'd appreciate a clarification so I can stop looking like an idiot). Every time I've come across the term in a computing context, it's either meant:

  • You never do anything that doesn't have a fixed upper bound for its time cost (and if you might do that anyway, you have a way to pre-empt it, e.g. an interrupt firing that forces a context switch) so you can guarantee things happen at/by a particular time. This is used for things like microcontrollers used for safety-critical systems that guarantee a certain consistent throughput of instructions by not being superscalar or not having cache so all instructions always take a predictable number of cycles, and for things like the Linux kernel gaining a real-time mode in the upcoming 6.12 release.
  • A paper on a graphics technique claims to be viable for video games because it 'only' takes 100ms for a contrived scene with no other interesting effects.

Clearly we're not using the second definition here as it's silly. There are plenty of event-driven real-time frameworks for programming microcontrollers, so the first definition can't be fundamentally incompatible, either.

As I thought we were using the first definition, I felt it was worth mentioning that the current approach has the same problem a naive event-driven approach would, as a burst of events can delay a frame for an arbitrarily long time as all the vsg::Window subclasses currently drain the entire event queue, which can be arbitrarily large and continue to grow as it's being drained. Even if that's not the appropriate definition to use, that's something that seems like a problem.

@AnyOldName3
Copy link
Contributor Author

I found some discussion with an eventual fix (that I've not looked at yet) when SDL ran into the same problem and took years to find a nice way to fix it: libsdl-org/SDL#1059. One of the things that was mentioned is that MacOS has an analogous problem where pressing and holding one of the window buttons will block for basically the same reasons, so it's just X and Wayland that don't have this problem.

@robertosfield
Copy link
Collaborator

robertosfield commented Oct 11, 2024

Thanks for the SDL discussion link. It's an interesting read. One thing missing is that folks are being frustrated with SDL but in fact no one is pointing the finger at the real culprint, Microsoft. This bit of the Win32 API is clearly a big fuck up, I'm not saying this lightly, it's really bad design. All the problems that have folks chasing their tales, wasting weeks work on these errors are all down to shoddy design & implementation that shouldn't got into the final release.

However, Microsoft has long been able to ignore better engineered prior art like unix and X11 and come up with their own bodge together shit show but through force of marketing and manipulative practices forced it's way to to top, despite poor engineering from the bottom up. It's become such a pervasively perverse situation that competent engineers have been so used to bad design from MS they normalize it and weave it into their own understanding of how to do things and not realize they are bound to the same engineering shortsightedness.

The reason why the OSG, VSG and SDL's polling of events is being tripped up by windows is not because polling of events is flawed, it because Win32 has elements of really bad design and implementation. Polling of events should never take more than a fraction of millisecond, it should never break a single frame let alone completely stall the calling application.

It's a MS fuck up through and through. The fact the Apple also have this buggy bad design doesn't make either parties approach excusable. It's just really bad. Neither operating system should be deployed in anything mission critical or real-time if they can't get even really basic things right.

An not, moving to event driven application doesn't fix these fundamental problems w.r.t real time applications, it just hides the glaring obvious MS bad design highlight by resize, you simply can't time a frame begin/swap buffers around when an OS decides it's time for a idle or timer event.

For real-time graphics app the thing that drives the frame is the swap buffers of the display, potentially across multiple displays across multiple machines. The VSG doesn't yet have support for the later out of the box as it'll require extensions and some hardware to work with, but the fundamental design is ready for it.

@AnyOldName3
Copy link
Contributor Author

Even for web applications, where there's no way for Windows details to pollute the environment, lots of UI frameworks opt into the same general approach as Win32. It's definitely not the best thing to make the only option (as you can't feasibly build a classic frame loop on top of it, but you can build it on top of a classic frame loop), but people do like it and opt into it when they don't have to forty years later. I don't like it, either, and using this approach for window resizing in particular is egregiously obtrusive, but it's not as simple as being a terrible decision everyone hates made for stupid reasons that no one understands. It's pretty clear that Microsoft weren't expecting to maintain binary backwards compatibility for Windows 1.0 applications until Windows 10 when they designed it, and how it would work (or as it turned out, not) for multimedia applications a decade before machines running Windows were typically capable of full colour was clearly not a priority. It'd certainly be less hassle to make a frame loop if there was a DefWindowProcEx that never blocked or tried running its own message pump.

As a specific nitpick, X11 isn't prior art here. X as a whole was initially released in mid-1984, pretty late into Windows' development (with the 1.0 release coming a year later) and had frequent breaking changes until X11 was released in 1987, two years after Windows. It's also notorious for not being well-engineered, hence being unmaintained for twelve years, and an inordinate amount of ongoing effort towards developing Wayland and porting everything over to it.

you simply can't time a frame begin/swap buffers around when an OS decides it's time for a idle or timer event.

This is:

  • what the VSG is already doing on every platform, including X11, as it's draining the whole event queue, so the OS already has to decide we're idle. Because X11/XCB are designed differently, it'd be trivial to fix by breaking out of the while loop when too close to a frame deadline.
  • not necessary on Windows, either, if it's used by-the-book. Our Win32WindowProc/lpfnWndProc callback gets first dibs at any event. That's our opportunity to decide we don't have time to process it, and need to draw a frame first, but can't do that without something like this PR that gives it the ability to draw a frame. We could guarantee only a few cycles are spent outside our lpfnWndProc callback at a time by not calling ValidateRect in response to WM_PAINT (which we currently do), as that's what controls whether GetMessage/PeekMessage block/return false, or create a new WM_PAINT message (although there's an extra flag for PeekMessage to prevent it from yielding - not necessary for GetMessage as it's a spinlock). If our callback runs every iteration and nothing blocks or yields, I don't see how it could make any difference whether it's two nested loops (one in windowing system code and one in our application) or just the one in our application. The timer approach SDL took isn't great as the minimum timer is 10ms and may get delayed until other events are done processing, but then SDL already doesn't have a way to consume only a subset of events before drawing a frame, so it would have to wait until all other events are done processing anyway.

As evidenced by the comparative size of those two bullet points, it's obviously way less hassle and hoop-jumping with X11, but it shouldn't be insurmountable on Windows, and even if I'm missing something that you've thought of and I don't know about, it should at least be possible to get pretty close.

@robertosfield
Copy link
Collaborator

The problem with the Win32 API is not that it supports event driven applications, it's that it breaks any sensible frame driven or similar non event driven applications, it's why the design and implementation is broken. API should support both event driven and non event driven usages. It should be trivial to do but it isn't, and causes really ugly glitches like we have, glitches that are hard to track down and fix, and ones that spill over to application/end user domain as demonstrated by the SDL thread.

I am bit lost on the whole process a subset of events if there isn't time budget to do it. The getting of all the events from the windowing API should be trivially fast. If application won't to process those events in expensive ways then it's up to the applications on how to handle them. Interactive applications won't care about frame drops in the same way a visual simulation or VR application will, for the later the application won't be resizing the windows etc. at critical times in the simulation so these type of expensive window related events will be a none issue.

What I'd like for us to achieve is to avoid the frame blocks that are happening in Win32_Window.cpp during the window resize. I want us to get to a place that on all platforms things just work, whether event driven like a Qt app or frame driven like a simulation/VR application should be. Things already work how they should work on X11/Xcb so it's a case of bring Win32 and macOS if it's not up to scratch.

This is not to say say that X11/Xcb are superior for all purposes, it's just that they work well for how simulation/VR application need windowing and event handling to function i.e. do what we ask from them and otherwise keep out of our way.

@AnyOldName3
Copy link
Contributor Author

The way I'd be inclined to do things (after a visit to the pub, so it's not to be taken as gospel) would be to give vsg::Viewer a run and frame function (which osg::Viewer had, so it wouldn't be a wild deviation) so we could hide the platform-specific details from the user and implement the best approach per platform (it could be a virtual function so users could subclass Viewer to override it or a std::function to override it without subclassing). If frame didn't call pollEvents itself, we'd then be free to rearrange things a bit, leaving X11/XCB (and Wayland if we get around to giving it a vsg::Window subclass) with the current frame loop approach (calling pollEvents() and frame() in a loop in run()), but Windows would pass frame as callback to the Win32_Window to be called in response to the WM_PAINT event and run would just call pollEvents in a loop, and MacOS would do whatever the equivalent is there. That way, we'd get the minimum latency and maximum throughput possible on each platform, and wouldn't need users to know about the edritch horrors that William Gates II foisted on us.

I am bit lost on the whole process a subset of events if there isn't time budget to do it. The getting of all the events from the windowing API should be trivially fast. If application won't to process those events in expensive ways then it's up to the applications on how to handle them. Interactive applications won't care about frame drops in the same way a visual simulation or VR application will, for the later the application won't be resizing the windows etc. at critical times in the simulation so these type of expensive window related events will be a none issue.

Part of why I mentioned this is that I've meddled with things running on microcontrollers where I had to cap the number of characters I'd read from a serial connection otherwise I'd miss my chance to poll pins (the Arduino Uno only has two interrupt-capable pins) or send a pulse to a stepper motor driver. There's a much bigger gap in computer graphics between the cost of draining the event queue and drawing a whole frame than there is between reading a command from a serial connection and the number of cycles between motor encoder pulses on a microcontroller, so it's a silly concern here.

The other part is that when ValidateRect (which tells Windows we're done painting a region so don't need to repaint it again until the user does something) isn't called, the delay between first calling PeekMessage (provided the non-yielding flag is passed) or GetMessage and our window message callback getting a WM_PAINT event is no longer than the delay between xcb_poll_for_event and a future call to xcb_poll_for_event returning a null pointer. If the former is scary because of potential delays, then the latter should be just as much of a concern.

@robertosfield
Copy link
Collaborator

I'm back in the office again after a being away since last Wednesday. I will tag a dev release today to at least give us a stable reference point for future work, even if it doesn't solve all the outstanding issues.

After posting these dev releases I'll work on the viewport sizing state issue and Win32_Widow.cpp issues with event handling. For the later I'm now inclined toward having a thread per vsg::Win32_Window, creating this thread in the constructor when the window is created locally and needs to handle the events. I think for cases like the vsgQt usage which provides the window and event handling this won't be required.

With the threading of the event loop, the easiest implementation would be to have a thread per vsg::Win32_Window object, but would we need a single event loop thread per application that handles multiple windows? If the later do the events have user data about which window they are associated so we can direct it at the appropriate Win32_Window's event queue?

@AnyOldName3
Copy link
Contributor Author

The first parameter to the lpfnWndProc is a HWND window handle for the window it's being called for, so that aspect should be easily doable.

@robertosfield
Copy link
Collaborator

Do you think a single thread for creating the native windows and running an event loop that gets the events and then adds them to the appropriate Win32_Window event queue is appropriate or have a single thread per Win32_Window?

@AnyOldName3
Copy link
Contributor Author

Both are probably viable. I'd hope that the nested message pumps run by message handlers when they don't immediately return would be capable of receiving and dispatching messages for other windows - at least in Wine, the SC_SIZE handler doesn't pass a window handle into GetMessage, so should be receiving messages for any window owned by the current thread. One thing to watch out for is that the documentation says a window-owning thread should only block on MsgWaitForMultipleObjects or MsgWaitForMultipleObjectsEx so it resumes when new messages arrive, but I don't think that means you can't take a lock on a mutex to forward data to the main thread - the big two concerns are ensuring messages don't have to wait a long time to be handled (you'll get a popup saying an application has frozen if you're not running it with a debugger attached and any messages spend more than five seconds in the queue) and you don't get deadlocks if another thread sends a message to a window with a blocking function, but the thread with the handler is waiting on a mutex the other thread holds.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants