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

State of the Windows platform ? #1429

Closed
filnet opened this issue Feb 1, 2020 · 17 comments
Closed

State of the Windows platform ? #1429

filnet opened this issue Feb 1, 2020 · 17 comments
Labels
DS - windows S - meta Project governance

Comments

@filnet
Copy link
Contributor

filnet commented Feb 1, 2020

Hi,

As a foreword, I am new to Rust and winit. Also, English is not my native language, so please take what follows with a grain of salt.

My understanding about winit on Windows is that:

  • it was recently updated quite heavily (in 0.20.0) to go from multi threaded to single threaded.
  • it has some complexity due to how Windows handles resizing and moving windows (switches to a modal mode where event query methods do not return anymore).

I've been looking at the code to address some issues [1][2][3] and it looks like the Windows code has inherited some complexity from the time it was multi threaded (?).

Thing's I have noticed:

  • there is some code duplication regarding event handling and runner state changes (in runner::process_event, runner::main_events_cleared and runner::draw_events_cleared)
  • state changes are located a bit everywhere and are not always consistent or enforced.
  • DeferredNewEvents handling seems brittle in particular when control flow is WaitUntil.
  • [nitpick] why the single runner.rs file in the event_loop directory

I am open to be given directions or to discuss ways to improve the situation if it needs to.

[1] #1391
[2] #1427
[3] #1428

PS: Looks like some projects are rolling back their winit 0.20.0 update because of issue [1].
See aclysma/skulpin#39

PPS : I have two PRs open :

@goddessfreya goddessfreya added DS - windows S - meta Project governance labels Feb 1, 2020
@filnet
Copy link
Contributor Author

filnet commented Feb 3, 2020

One concrete example:

pub(crate) struct SubclassInput<T: 'static> {
    pub window_state: Arc<Mutex<WindowState>>,
    pub event_loop_runner: EventLoopRunnerShared<T>,
    pub file_drop_handler: FileDropHandler,
}

Is the Mutex protecting WindowState still needed ?

@filnet
Copy link
Contributor Author

filnet commented Feb 8, 2020

@Osspial, I believe this recent change has broken the Windows platform in a few ways : 6a330a2#diff-d4d8c8dcf07874a35d08c6a70e0a6daf

It most probably introduced this regression: #1427
The change introduced a borrow() that can fail and fails. Similar borrows done right after it use try_borrow() and handle the case when it fails.

I am also pretty sure it introduced #1391.

And I found a suspect code change:

               if 0 == winuser::PeekMessageW(&mut msg, ptr::null_mut(), 0, 0, 0) {
                    if msg.message != 0 && msg.message != winuser::WM_PAINT {
                        queue_call_again();
                        return 0;

Here the test on msg after the PeekMessageW() call is pointless as we are in the case where there is no message.

I will try to revert the fix and reproduce the missing RedrawRequested issue.
Were there any specifics needed to reproduce the issue?

@filnet
Copy link
Contributor Author

filnet commented Feb 8, 2020

PS: there is an open PR for issue #1391 : #1422

But I am not too happy about it...

@filnet
Copy link
Contributor Author

filnet commented Feb 9, 2020

I reverted 6a330a2 and was able to reproduce an issue that looks similar to the one addressed by that fix.

If the user calls window.request_redraw() while handling the MainEventsCleared event, then the RedrawRequested event is sent on the next loop iteration and not the current iteration.
Calling window.request_redraw() while handling the other events works fine.

I am now trying to fix the issue in a different way...

@filnet
Copy link
Contributor Author

filnet commented Feb 9, 2020

#1427 was closed as a duplicate of #1400

@filnet
Copy link
Contributor Author

filnet commented Feb 9, 2020

For reference:

    /// Emits a `WindowEvent::RedrawRequested` event in the associated event loop after all OS
    /// events have been processed by the event loop.
    ///
    /// This is the **strongly encouraged** method of redrawing windows, as it can integrate with
    /// OS-requested redraws (e.g. when a window gets resized).
    ///
    /// This function can cause `RedrawRequested` events to be emitted after `Event::MainEventsCleared`
    /// but before `Event::NewEvents` if called in the following circumstances:
    /// * While processing `MainEventsCleared`.
    /// * While processing a `RedrawRequested` event that was sent during `MainEventsCleared` or any
    ///   directly subsequent `RedrawRequested` event.
    ///
    /// ## Platform-specific
    ///
    /// - **iOS:** Can only be called on the main thread.
    #[inline]
    pub fn request_redraw(&self) {
        self.window.request_redraw()
    }

My understanding is that calling request_redraw should cause an RedrawRequested event to be sent during the current loop iteration not matter when this is called (when handling a MainEventsCleared event or even a RedrawRequested (possible infinite loop here?)

@filnet
Copy link
Contributor Author

filnet commented Feb 9, 2020

request_redraw in 0.21.0

    pub fn request_redraw(&self) {
        unsafe {
            winuser::RedrawWindow(
                self.window.0,
                ptr::null(),
                ptr::null_mut(),
                winuser::RDW_INTERNALPAINT,
            );
        }
    }

request_redraw in 0.20.0-alpha1

   #[inline]
    pub fn request_redraw(&self) {
        unsafe {
            if self.thread_executor.trigger_newevents_on_redraw() {
                winuser::RedrawWindow(
                    self.window.0,
                    ptr::null(),
                    ptr::null_mut(),
                    winuser::RDW_INTERNALPAINT,
                );
            } else {
                winuser::PostMessageW(self.window.0, *REQUEST_REDRAW_NO_NEWEVENTS_MSG_ID, 0, 0);
            }
        }
    }

@filnet
Copy link
Contributor Author

filnet commented Feb 10, 2020

What I am seeing after reverting the commit is confirmed by the associated PR description (which I overlooked) : #1366

Continuing my effort to understand and fix request_redraw on Windows, I did a bit of forensic and looked at how other platforms handle it.

On Windows, the current approach is to post a WM_PAINT message.
Older versions (0.20.0-alpha) also posted a REQUEST_REDRAW_NO_NEWEVENTS_MSG_ID message to distinguish request_redraw called after or before events were cleared. Not sure why.
These approaches are tricky because you need to dispatch these messages after clearing the main events.

Other platforms use a different approach: they raise a flag or buffer the redraw internally when request_redraw is called.

X11:

    pub fn request_redraw(&self) {
        self.pending_redraws
            .lock()
            .unwrap()
            .insert(WindowId(self.xwindow));
    }

Wayland:

    pub fn request_redraw(&self) {
        *self.need_refresh.lock().unwrap() = true;
    }

These approaches seem much easier to handle. Problem is that on Windows, the window struct does not have access to the event_loop or runner.

@Osspial any thoughts on that ?

@Osspial
Copy link
Contributor

Osspial commented Feb 18, 2020

Hey, thanks for opening an issue to keep track of all this. I'll write up a more thorough response within the next couple days when I've got the time to look into this more. Sorry I didn't get back on this sooner - I was fairly stressed over the past month and wasn't keeping tabs on Winit to help manage that, but I'm feeling quite a bit better now and should be able to respond more promptly for the foreseeable future.

These approaches seem much easier to handle. Problem is that on Windows, the window struct does not have access to the event_loop or runner.

@Osspial any thoughts on that ?

The rationale behind using WM_PAINT is that that's the best way to get the OS to merge user-requested and OS-requested redraw events. We could theoretically keep track of that information ourselves and emit RedrawRequested events ourselves when the event loop has been drained, but Windows strongly prefers that you execute your redrawing code in the WM_PAINT handler rather than outside of it. That creates a tricky balance for Winit, since it's difficult to both satisfy its external API and winapi's preferences at the same time. #1366 was my initial attempt to address that, which... clearly didn't work out all that great. I've got ideas on how to handle that better, but it's been a bit since I've looked at this code closely so they aren't coming to mind at this particular moment.

@filnet
Copy link
Contributor Author

filnet commented Feb 22, 2020

Hi @Osspial,

Thanks for getting back!

I have looked at the request_redraw vs WM_PAINT issue quite a bit these last couple of weeks.
I looked at older 0.20.0 alpha versions and at your recent attempt at fixing related issues.

I also looked at how other platforms handle it and came up with this approach: #1461. Please take a look.

We can discuss it further here or in the PR.

@filnet
Copy link
Contributor Author

filnet commented Feb 24, 2020

If redrawing must occur only when receiving a WM_PAINT message then my PR does not comply.

I am now looking into another approach using UpdateWindow instead. It looks promising.
Calling UpdateWindow from request_redraw will cause the WM_PAINT message to be sent immediately. This removes the need to Peek/Get/Dispatch messages after calling main_events_cleared() in the event the user calls request_redraw when handling the MainEventsCleared event (which is the root of the problems).

Looks like RedrawWindow can work in that mode too if the RDW_UPDATENOW flag is set.

filnet added a commit to filnet/winit that referenced this issue Feb 25, 2020
@filnet
Copy link
Contributor Author

filnet commented Feb 25, 2020

The UpdateWindow approach was a dead end.

So I went full circle and simply fixed the various panics using the current approach of calling RedrawWindow.
I believe that all panics are now fixed. The code is also simpler now.

Use cases that have been tested:

  • calling request_redraw when handling the MainEventsCleared event in all control flow modes
  • same as above but calling request_redraw multiple times
  • calling request_redraw from a thread other than the event loop thread
  • multiwindow

A few corner cases have not been tested:

  • calling request_redraw when handling events other than MainEventsCleared
  • changing window state and triggering events when handling events (in particular when handling MainEventsCleared, RedrawRequested and RedrawEventsCleared)
    The specs are not very clear about such use cases.

filnet added a commit to filnet/winit that referenced this issue Feb 25, 2020
filnet added a commit to filnet/winit that referenced this issue Feb 25, 2020
@filnet
Copy link
Contributor Author

filnet commented Feb 25, 2020

The fullscreen and window_debug examples still panic when entering borderless or exclusive full screen mode. See #1469.

The min_max_size example also panics (but not in master).

Looking into it now.

filnet added a commit to filnet/winit that referenced this issue Feb 26, 2020
@filnet
Copy link
Contributor Author

filnet commented Feb 26, 2020

The remaining panics are all caused by calls to UpdateWindow.
UpdateWindow will directly send a WM_PAINT message which might, in some cases, cause RedrawRequested events to be buffered.
The runner is currently not able to correctly handle buffered RedrawRequested events and state transition will fail to be done orderly.
Additionally we also want to handle RedrawRequested events during the processing of WM_PAINT messages (to make Windows happy ?). Buffering prevents this.

For now, all calls to UpdateWindow were changed to calls to InvalidateRgn.
And buffering a RedrawRequested event will now cause a panic.

@filnet
Copy link
Contributor Author

filnet commented Feb 27, 2020

And back to square one :( : #1461 (comment)

Seems like PeekMessage can, under some conditions, dispatch messages [1][2].
The non modal event loop looks like this (pseudo code):

looop {
    runner.new_events();
    while PeekMessage(msg) != WM_PAINT {
        DispatchMessage(msg);
    }
    runner.main_events_cleared();
    // Drain eventual WM_PAINT messages sent if user called request_redraw() during handling of MainEventsCleared.
    while PeekMessage(msg) == WM_PAINT {
        DispatchMessage(msg);
    }
    runner.redraw_events_cleared();
}

Trouble is that the loop to drain WM_PAINT messages can end up dispatching messages other than WM_PAINT. These are then sent to the runner but the runner is in MainEventsCleared state and cannot handler them.
I have seen that issue before but cannot reproduce it systematically.

This is also interesting https://devblogs.microsoft.com/oldnewthing/?p=36493

[1] https://www.gamedev.net/forums/topic/685863-win32-message-handling-questions/
[2] https://www.gamedev.net/forums/topic/670857-peekmessage-changed-in-windows-10/

filnet added a commit to filnet/winit that referenced this issue Feb 28, 2020
@filnet
Copy link
Contributor Author

filnet commented Feb 28, 2020

Here is a backtrace that shows that PeekMessage sends messages. This is mentioned in doc.
This happens while draining WM_PAINT messages after clearing the main events.

MainEventsCleared
thread 'main' panicked at 'non-redraw event in redraw phase: Some(WindowEvent { window_id: WindowId(WindowId(0x310646)), event: KeyboardInput { device_id: DeviceId(DeviceId(0)), input: KeyboardInput { scancode: 0, state: Released, virtual_keycode: None, modifiers: (empty) }, is_synthetic: true } })', src\platform_impl\windows\event_loop\runner.rs:375:17
stack backtrace:
   0:   0xe7b009 - core::fmt::write
                       at /rustc/73528e339aae0f17a15ffa49a8ac608f50c6cf14\/src\libcore\fmt\mod.rs:1028
   1:   0xe65691 - std::io::Write::write_fmt<std::sys::windows::stdio::Stderr>
                       at /rustc/73528e339aae0f17a15ffa49a8ac608f50c6cf14\/src\libstd\io\mod.rs:1412
   2:   0xe6a17a - std::panicking::default_hook::{{closure}}
                       at /rustc/73528e339aae0f17a15ffa49a8ac608f50c6cf14\/src\libstd\panicking.rs:188
   3:   0xe69da5 - std::panicking::default_hook
                       at /rustc/73528e339aae0f17a15ffa49a8ac608f50c6cf14\/src\libstd\panicking.rs:205
   4:   0xe6a818 - std::panicking::rust_panic_with_hook
                       at /rustc/73528e339aae0f17a15ffa49a8ac608f50c6cf14\/src\libstd\panicking.rs:464
   5:   0xe6a419 - std::panicking::continue_panic_fmt
                       at /rustc/73528e339aae0f17a15ffa49a8ac608f50c6cf14\/src\libstd\panicking.rs:373
   6:   0xe6a343 - std::panicking::begin_panic_fmt
                       at /rustc/73528e339aae0f17a15ffa49a8ac608f50c6cf14\/src\libstd\panicking.rs:328
   7:   0xe061f6 - winit::platform_impl::platform::event_loop::runner::EventLoopRunner<()>::process_event<()>
                       at C:\msys64\home\philippe.renon\winit\src\platform_impl\windows\event_loop\runner.rs:375
   8:   0xe04d88 - winit::platform_impl::platform::event_loop::runner::ELRShared<()>::send_event_unbuffered<()>
                       at C:\msys64\home\philippe.renon\winit\src\platform_impl\windows\event_loop\runner.rs:121
   9:   0xe049bf - winit::platform_impl::platform::event_loop::runner::ELRShared<()>::send_event<()>
                       at C:\msys64\home\philippe.renon\winit\src\platform_impl\windows\event_loop\runner.rs:106
  10:   0xd98317 - winit::platform_impl::platform::event_loop::SubclassInput<()>::send_event<()>
                       at C:\msys64\home\philippe.renon\winit\src\platform_impl\windows\event_loop.rs:104
  11:   0xd9c0b9 - winit::platform_impl::platform::event_loop::public_window_callback<()>
                       at C:\msys64\home\philippe.renon\winit\src\platform_impl\windows\event_loop.rs:1320
  12: 0x6548a9c0 - DefSubclassProc
  13: 0x6548a8b4 - DPA_Clone
  14: 0x7642630a - gapfnScSendMessage
  15: 0x76426d4a - GetThreadDesktop
  16: 0x76426df8 - GetThreadDesktop
  17: 0x76426e54 - GetThreadDesktop
  18: 0x7763010a - KiUserCallbackDispatcher
  19: 0x76426a9f - gapfnScSendMessage
  20: 0x76426afc - gapfnScSendMessage
  21: 0x7642630a - gapfnScSendMessage
  22: 0x76426d4a - GetThreadDesktop
  23: 0x76430d57 - GetClientRect
  24: 0x76430d7d - CallWindowProcW
  25: 0x6548a7b3 - DPA_Clone
  26: 0x6548a9c0 - DefSubclassProc
  27: 0x6548a975 - DefSubclassProc
  28:   0xd9d90f - winit::platform_impl::platform::event_loop::public_window_callback<()>
                       at C:\msys64\home\philippe.renon\winit\src\platform_impl\windows\event_loop.rs:1672
  29: 0x6548a9c0 - DefSubclassProc
  30: 0x6548a8b4 - DPA_Clone
  31: 0x7642630a - gapfnScSendMessage
  32: 0x76426d4a - GetThreadDesktop
  33: 0x76426df8 - GetThreadDesktop
  34: 0x76426e54 - GetThreadDesktop
  35: 0x7763010a - KiUserCallbackDispatcher
  36: 0x76430781 - PeekMessageW
  37:   0xd97dd3 - winit::platform_impl::platform::event_loop::EventLoop<()>::run_return<(),closure-0>
                       at C:\msys64\home\philippe.renon\winit\src\platform_impl\windows\event_loop.rs:250
  38:   0xd981f8 - winit::platform_impl::platform::event_loop::EventLoop<()>::run<(),closure-0>
                       at C:\msys64\home\philippe.renon\winit\src\platform_impl\windows\event_loop.rs:192
  39:   0xda31a7 - winit::event_loop::EventLoop<()>::run<(),closure-0>
                       at C:\msys64\home\philippe.renon\winit\src\event_loop.rs:148
  40: 0x7763010a - KiUserCallbackDispatcher
  41:   0xd956e1 - std::rt::lang_start::{{closure}}<()>
                       at /rustc/73528e339aae0f17a15ffa49a8ac608f50c6cf14\src\libstd\rt.rs:61
  42:   0xe687a4 - std::sys_common::backtrace::__rust_begin_short_backtrace<closure-0,i32>
  43:   0xe6a2b1 - std::panicking::try::do_call<closure-0,i32>
                       at /rustc/73528e339aae0f17a15ffa49a8ac608f50c6cf14\/src\libstd\panicking.rs:287
  44:   0xe6cf83 - panic_unwind::__rust_maybe_catch_panic
                       at /rustc/73528e339aae0f17a15ffa49a8ac608f50c6cf14\/src\libpanic_unwind\lib.rs:78
  45:   0xe6ab2f - std::rt::lang_start_internal
                       at /rustc/73528e339aae0f17a15ffa49a8ac608f50c6cf14\/src\libstd\rt.rs:47
  46:   0xd956bd - std::rt::lang_start<()>
                       at /rustc/73528e339aae0f17a15ffa49a8ac608f50c6cf14\src\libstd\rt.rs:61
  47:   0xe7ed8c - __scrt_common_main_seh
                       at d:\agent\_work\6\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288

In this case it is a WM_SETFOCUS that gets sent. When handling this message, winit will first emit a WindowEvent::KeyboardInput event (no idea why...) and then a WindowEvent::Focused event.
But the WindowEvent::KeyboardInput causes the panic because the runner has cleared the main events and is in the HandlingRedraw state.

A fix that seems to work is to pass the PM_QS_PAINT flag when peeking. This seems to prevent unwanted messages from being sent.

Edited to add this link : Non-Queued messages

Edited: it is PM_QS_PAINT not QS_PAINT that needs to be passed to the peek function.

filnet added a commit to filnet/winit that referenced this issue Mar 6, 2020
Osspial pushed a commit that referenced this issue Mar 7, 2020
* On Windows, fix request_redraw() related panics

These panics were introduced by 6a330a2

Fixes #1391
Fixes #1400
Fixes #1466
Probably fixes other related issues

See #1429

* On Windows, replace all calls to UpdateWindow by calls to InvalidateRgn

This avoids directly sending a WM_PAINT message,
which might cause buffering of RedrawRequested events.

We don't want to buffer RedrawRequested events because:
- we wan't to handle RedrawRequested during processing of WM_PAINT messages
- state transitionning is broken when handling buffered RedrawRequested events

Fixes #1469

* On Windows, panic if we are trying to buffer a RedrawRequested event

* On Windows, move modal loop jumpstart to set_modal_loop() method

This fixes a panic.
Note that the WM_PAINT event is now sent to the modal_redraw_method
which is more correct and avoids an unecessary redraw of the window.

Relates to but does does not fix #1484

* On Window, filter by paint messages when draining paint messages

This seems to prevent PeekMessage from dispatching unrelated sent messages

* Change recently added panic/assert calls with warn calls

This makes the code less panicky...

And actually, winit's Windoww callbacks should not panic
because the panic will unwind into Windows code.

It is currently undefined behavior to unwind from Rust code into foreign code.
See https://doc.rust-lang.org/std/panic/fn.catch_unwind.html

* add comments to clarify WM_PAINT handling in non modal loop

* made redraw_events_cleared more explicit and more comments
@filnet
Copy link
Contributor Author

filnet commented Mar 8, 2020

We can close this issue now.

@Osspial Osspial closed this as completed Mar 8, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
DS - windows S - meta Project governance
Development

No branches or pull requests

3 participants