jonathanhogg / flitter

A functional programming language and declarative system for describing 2D and 3D visuals
https://flitter.readthedocs.io
BSD 2-Clause "Simplified" License
34 stars 1 forks source link

Windowed mode not working on macOS Sonoma (Apple Silicon) #22

Closed mdales closed 8 months ago

mdales commented 8 months ago

I checked out and tried to run flitter as described in the readme, and running the demo crashes:

(venv)  venv  kladdkaka  flitter  main  $  flitter examples/hoops.fl                                                                                 15:17 
15:17:42.969 93674:.engine.control  | SUCCESS: Loaded page 0: examples/hoops.fl
/Users/michael/Dev/flitter/venv/lib/python3.11/site-packages/glfw/__init__.py:916: GLFWError: (65540) b'Invalid window size 0x0'
  warnings.warn(message, GLFWError)
15:17:43.238 93674:.engine.__main__ | ERROR: Unexpected exception in flitter
Traceback (most recent call last):
  File "/Users/michael/Dev/flitter/venv/bin/flitter", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "/Users/michael/Dev/flitter/venv/lib/python3.11/site-packages/flitter/engine/__main__.py", line 65, in main
    asyncio.run(controller.run())
  File "/opt/homebrew/Cellar/python@3.11/3.11.6/Frameworks/Python.framework/Versions/3.11/lib/python3.11/asyncio/runners.py", line 190, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.11/3.11.6/Frameworks/Python.framework/Versions/3.11/lib/python3.11/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.11/3.11.6/Frameworks/Python.framework/Versions/3.11/lib/python3.11/asyncio/base_events.py", line 653, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "/Users/michael/Dev/flitter/venv/lib/python3.11/site-packages/flitter/engine/control.py", line 226, in run
    self._references = await self.update_renderers(context.graph, **names)
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/michael/Dev/flitter/venv/lib/python3.11/site-packages/flitter/engine/control.py", line 116, in update_renderers
    await asyncio.gather(*tasks)
  File "/Users/michael/Dev/flitter/venv/lib/python3.11/site-packages/flitter/render/window/__init__.py", line 121, in update
    self.create(engine, node, resized, **kwargs)
  File "/Users/michael/Dev/flitter/venv/lib/python3.11/site-packages/flitter/render/window/__init__.py", line 457, in create
    self.recalculate_viewport(new_window)
  File "/Users/michael/Dev/flitter/venv/lib/python3.11/site-packages/flitter/render/window/__init__.py", line 526, in recalculate_viewport
    if width / height > aspect_ratio:
       ~~~~~~^~~~~~~~
ZeroDivisionError: division by zero

The root cause of this issues is that the call to get the monitor working area returns (0, 44, 0, 0) - this is not an error (I can call glfw.get_error() and I get 0, and the 44 implies we know about the menu bar.

This response gives mw and mh as zero, which causes the following code:

            while width > mw * 0.95 or height > mh * 0.95:
                width = width * 2 // 3
                height = height * 2 // 3

to make width and height zero.

I did some spelunking, and if I comment that code out so I get a window, then even with a fully setup and working GL environment the monitor area is still wrong. In fullscreen things work, as this check then doesn't have effect.

macOS version: 14.0 (23A344 Python version: Python 3.11.6 (from home-brew) CPU: Apple M1 Pro

jonathanhogg commented 8 months ago

When you get a chance, could you try the following in the Python venv:

>>> import glfw
>>> glfw.init()
1
>>> glfw.get_monitors()
[<glfw.LP__GLFWmonitor object at 0x106930cb0>]
>>> for monitor in _:
...     print(glfw.get_monitor_workarea(monitor))
... 
(0, 25, 1792, 1095)
>>> 

Thanks! I'm curious if there's some pseudo-monitor being returned by this.

mdales commented 8 months ago

Alas, there is just one monitor:

(venv) $ python3   
Python 3.11.6 (main, Oct  2 2023, 13:45:54) [Clang 15.0.0 (clang-1500.0.40.1)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import glfw
>>> glfw.init()
1
>>> glfw.get_monitors()
[<glfw.LP__GLFWmonitor object at 0x102c4dcd0>]
>>> for monitor in _:
...     print(glfw.get_monitor_workarea(monitor))
... 
(0, 44, 0, 0)
jonathanhogg commented 8 months ago

Hmmm… sad news. It feels like it's a bug upstream somewhere. pyGLFW is a fairly thin shim over GLFW, so I'm guessing the latter. I suspect I'm gonna have to upgrade to Sonoma to debug it.

If fullscreen operation is working, it'd be good if you could test a few of the examples and see if they all work before I commit 😉

Note that fullscreen operation with a single monitor isn't great with GFLW – another annoying issue. Generally I let is slide because I'm usually running fullscreen on a second monitor when working in the studio or performing live.

mdales commented 8 months ago

FWIW, I tried it with another monitor plugged in and I get:

>>> import glfw
>>> glfw.init()
1
>>> glfw.get_monitors()
[<glfw.LP__GLFWmonitor object at 0x10086dcd0>, <glfw.LP__GLFWmonitor object at 0x10086dd50>]
>>> for monitor in _:
...     print(glfw.get_monitor_workarea(monitor))
... 
(0, 25, 0, 0)
(3360, 765, 0, 0)

So still no width/height data. FWIW, I assume this is the glfw version I'm using:

$ brew info glfw
==> glfw: stable 3.3.8 (bottled), HEAD
Multi-platform library for OpenGL applications
https://www.glfw.org/
Not installed
From: https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/g/glfw.rb
License: Zlib
==> Dependencies
Build: cmake ✔
==> Options
--HEAD
    Install HEAD version
==> Analytics
install: 1,820 (30 days), 5,174 (90 days), 14,666 (365 days)
install-on-request: 1,514 (30 days), 4,234 (90 days), 12,364 (365 days)
build-error: 0 (30 days)

I note that this is the latest public release of glfw, but that was published in July 2022. https://www.glfw.org

I then thought I'd see if it's a generic GLFW issue on Sonoma, so I wrote the following small Go program:

import (
    "fmt"
    "github.com/go-gl/glfw/v3.3/glfw"
)

func main() {
    err := glfw.Init()
    if err != nil {
        panic(err)
    }
    defer glfw.Terminate()

    monitors := glfw.GetMonitors()
    for _, monitor := range monitors {
        x, y, width, height := monitor.GetWorkarea()
        fmt.Printf("%d %d %d %d\n", x, y, width, height);
    }
}

This is based on the sample code provided with the go-gl/glfw library. The output of this is:

0 25 3360 1865
3360 765 1800 1125

From this I infer that the issue is perhaps related to the Python bindings on Sonoma rather than the GLFW library itself.

In response to your question @jonathanhogg about the examples working or not, I've attached a couple of screenshots for you to decide if they work (the are in a window as I commented out the window resizing code). They generally seemed to work, except:

Screenshot 2023-11-13 at 13 00 03 Screenshot 2023-11-13 at 13 03 50 Screenshot 2023-11-13 at 13 04 34
jonathanhogg commented 8 months ago

Interesting…

So my understanding is that pyGLFW uses a built-in GLFW 3.3.8 shared library on macOS. I'll have a stare at the code and see if anything jumps out as to what is going wrong.

In terms of tests:

Of the working examples, smoke.fl and canvas3d.fl look as I'd expect, but the physic.fl screenshot looks odd as it looks like there are two balls of cells overlaid on top of each other and some resulting weird artefacts from the bloom filter. Is this exactly how it looked on-screen? I'm wondering if the double-buffering has gone mad…

jonathanhogg commented 8 months ago

The pyGLFW wrapper for glfwGetMonitorWorkArea() is pretty thin:

if hasattr(_glfw, 'glfwGetMonitorWorkarea'):
    _glfw.glfwGetMonitorWorkarea.restype = None
    _glfw.glfwGetMonitorWorkarea.argtypes = [ctypes.POINTER(_GLFWmonitor),
                                        ctypes.POINTER(ctypes.c_int),
                                        ctypes.POINTER(ctypes.c_int)]

    def get_monitor_workarea(monitor):
        """
        Retrives the work area of the monitor.

        Wrapper for:
            void glfwGetMonitorWorkarea(GLFWmonitor* monitor, int* xpos, int* ypos, int* width, int* height);
        """
        xpos_value = ctypes.c_int(0)
        xpos = ctypes.pointer(xpos_value)
        ypos_value = ctypes.c_int(0)
        ypos = ctypes.pointer(ypos_value)
        width_value = ctypes.c_int(0)
        width = ctypes.pointer(width_value)
        height_value = ctypes.c_int(0)
        height = ctypes.pointer(height_value)
        _glfw.glfwGetMonitorWorkarea(monitor, xpos, ypos, width, height)
        return (
            xpos_value.value,
            ypos_value.value,
            width_value.value,
            height_value.value
        )

I'm unclear why the width and height values wouldn't be updated on Sonoma/Apple-Silicon (not sure yet which is the root cause since I've not updated my Mac yet).

mdales commented 8 months ago
$ lsof | grep glfw
...
Python     9281 michael  txt       REG               1,17     233516            83660324 /Users/michael/Dev/flitter/venv/lib/python3.11/site-packages/glfw/libglfw.3.dylib
...

Looks like pip installs its own glfw lib?

mdales commented 8 months ago

Here's a video of physics.fl in action - to does look like it's doing everything twice somehow?

https://github.com/jonathanhogg/flitter/assets/28506/22b2d962-d99a-4c3e-83ab-0cfe79c1755d

jonathanhogg commented 8 months ago

Cripes. That is freaky! There appear to be – at least – two versions of the ball being rendered. It's not double-buffering as they are too far apart. It looks like the whole program is being run twice with two different beat clocks into the same render buffer.

Reckon that's another bug report in there, though…

mdales commented 8 months ago

On the monitors thing, it occurred to me to try rule out whether it was the fact I was using a home-brew install of Python (flitter needing > 3.9 which is still the system install on Sonoma). But if I run the sample code you gave me before using glfw installed in a virtual env using the system Python I get the same result of no width/height.

mdales commented 8 months ago

Getting the video mode does seem to work:

>>> m = glfw.get_monitors()[0]
>>> glfw.get_video_mode(m)
GLFWvidmode(size=Size(width=1800, height=1169), bits=Bits(red=8, green=8, blue=8), refresh_rate=120)
jonathanhogg commented 8 months ago

Sadly, this code is working fine for me on Intel Sonoma (homebrew Python 3.11.6 and glfw 2.6.2) so it seems to be a bug that only affects ARM64.

I was about to say that this is possibly a bug in ctypes when I noticed that the glfw wrapper for glfwGetMonitorWorkarea() looks to be setting up the argument types incorrectly. The wrapper has this in it (glfw/__init__.py:995):

    _glfw.glfwGetMonitorWorkarea.restype = None
    _glfw.glfwGetMonitorWorkarea.argtypes = [ctypes.POINTER(_GLFWmonitor),
                                        ctypes.POINTER(ctypes.c_int),
                                        ctypes.POINTER(ctypes.c_int)]

but the function takes 5 arguments: the monitor and 4 pointers to integers. I wonder if you could try changing this code to:

    _glfw.glfwGetMonitorWorkarea.restype = None
    _glfw.glfwGetMonitorWorkarea.argtypes = [ctypes.POINTER(_GLFWmonitor),
                                        ctypes.POINTER(ctypes.c_int),
                                        ctypes.POINTER(ctypes.c_int),
                                        ctypes.POINTER(ctypes.c_int),
                                        ctypes.POINTER(ctypes.c_int)]

and see if that works? If so, then I'll raise a bug upstream on PyGLFW. I've verified that making this change doesn't affect functionality on Intel.

mdales commented 8 months ago

Ding ding ding, you have a winner:

import glfw
import types
from glfw import _GLFWmonitor
>>> glfw.library.glfw.glfwGetMonitorWorkarea.argtypes = [ctypes.POINTER(_GLFWmonitor),
...         ctypes.POINTER(ctypes.c_int),
...         ctypes.POINTER(ctypes.c_int),
...         ctypes.POINTER(ctypes.c_int),
...         ctypes.POINTER(ctypes.c_int)]
>>> glfw.init()
1
>>> glfw.get_monitors()
[<glfw.LP__GLFWmonitor object at 0x100d0dd50>, <glfw.LP__GLFWmonitor object at 0x100d0ddd0>]
>>> for m in _:
...   print(glfw.get_monitor_workarea(m))
... 
(0, 25, 3360, 1865)
(3360, 765, 1800, 1125)
jonathanhogg commented 8 months ago

I've opened an upstream bug report and PR (https://github.com/FlorianRhiem/pyGLFW/issues/73).

jonathanhogg commented 8 months ago

A new version of glfw has just been released that fixes this bug. Can you confirm windows are working again on your Mac for me? Then I'll get back to staring at the 3D rendering bug…

mdales commented 8 months ago

I did a pip install --upgrade glfw and that seems to have fixed the issue.