ENH Support SDL-based packages and add pyxel (#3508)

Co-authored-by: Hood Chatham <roberthoodchatham@gmail.com>
Co-authored-by: Roman Yurchak <rth.yurchak@gmail.com>
This commit is contained in:
Gyeongjae Choi 2023-03-30 17:18:31 +09:00 committed by GitHub
parent 7f4f66b34b
commit 2046310460
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 317 additions and 3 deletions

View File

@ -89,6 +89,7 @@ dist/libpyodide.a: \
dist/pyodide.asm.js: \
src/core/main.o \
$(wildcard src/py/lib/*.py) \
libgl \
$(CPYTHONLIB) \
dist/libpyodide.a
date +"[%F %T] Building pyodide.asm.js..."
@ -206,6 +207,12 @@ dist/module_webworker_dev.js: src/templates/module_webworker.js
dist/webworker_dev.js: src/templates/webworker.js
cp $< $@
.PHONY: libgl
libgl:
# TODO(ryanking13): Link this to a side module not to the main module.
# For unknown reason, a side module cannot see symbols when libGL is linked to it.
embuilder build libgl
.PHONY: lint
lint:
pre-commit run -a --show-diff-on-failure

View File

@ -132,7 +132,13 @@ export MAIN_MODULE_LDFLAGS= $(LDFLAGS_BASE) \
-lproxyfs.js \
-lworkerfs.js \
-lwebsocket.js \
-leventloop.js
-leventloop.js \
\
-lGL \
-legl.js \
-lwebgl.js \
-lhtml5_webgl.js \
-sGL_WORKAROUND_SAFARI_GETCONTEXT_BUG=0
export SIDE_MODULE_CXXFLAGS = $(CXXFLAGS_BASE)

View File

@ -241,12 +241,15 @@ iterable`. (Python async _iterables_ that were not also iterators were already
### Packages
- New packages: fastparquet {pr}`3590`, cramjam {pr}`3590`, pynacl {pr}`3500`,
pyxel {pr}`3508`.
mypy {pr}`3504`, multidict {pr}`3581`, yarl {pr}`3702`, idna {pr}`3702`.
- Upgraded to micropip 0.3.0 (see
[changelog](https://github.com/pyodide/micropip/blob/main/CHANGELOG.md)
{pr}`3709`
- Added experimental [support for SDL based packages](using-sdl) {pr}`3508`
- Upgraded packages: see the list of packages versions in this release in
{ref}`packages-in-pyodide`.

View File

@ -519,7 +519,7 @@ class PyodideAnalyzer:
def get_val():
return OrderedDict([("attribute", []), ("function", []), ("class", [])])
modules = ["globalThis", "pyodide", "pyodide.ffi"]
modules = ["globalThis", "pyodide", "pyodide.ffi", "pyodide.canvas"]
self.js_docs = {key: get_val() for key in modules}
items = {key: list[Any]() for key in modules}
pyproxy_subclasses = []
@ -561,6 +561,10 @@ class PyodideAnalyzer:
# isinstance(doclet, Function) and doclet.is_static.
continue
if filename == "canvas.":
items["pyodide.canvas"].append(doclet)
continue
if filename == "pyproxy.gen." and isinstance(doclet, Class):
pyproxy_subclasses.append(doclet)

View File

@ -35,3 +35,15 @@ import type { PyProxy } from "pyodide/ffi";
.. js-doc-content:: pyodide.ffi
```
(js-api-pyodide-canvas)=
## pyodide.canvas
This provides APIs to set a canvas for rendering graphics.
```{eval-rst}
.. js-doc-summary:: pyodide.canvas
.. js-doc-content:: pyodide.canvas
```

View File

@ -189,4 +189,5 @@ main();
:hidden:
packages-in-pyodide.md
sdl.md
```

49
docs/usage/sdl.md Normal file
View File

@ -0,0 +1,49 @@
(using-sdl)=
# Using SDL-based packages in Pyodide
```{admonition} This is experimental
:class: warning
SDL support in Pyodide is experimental.
Pyodide relies on undocumented behavior of Emscripten and SDL,
so it may break or change in the future.
In addition, this feature requires to enable an opt-in flag,
`pyodide._api._skip_unwind_fatal_error = true;`
which can lead to stack unwinding issues (see {ref}`sdl-known-issues`).
```
Pyodide provides a way to use SDL-based packages in the browser,
This document explains how to use SDL-based packages in Pyodide.
## Setting canvas
Before using SDL-based packages, you need to set the canvas to draw on.
The `canvas` object must be a
[HTMLCanvasElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement) object,
with the `id` attribute set to `"canvas"`.
For example, you can set a canvas like this:
```js
let sdl2Canvas = document.createElement("canvas");
sdl2Canvas.id = "canvas";
pyodide.canvas.setCanvas2D(sdl2Canvas);
```
See also: {ref}`js-api-pyodide-canvas`
(sdl-known-issues)=
## Known issues
There is a known issue that with,
```
pyodide._api._skip_unwind_fatal_error = true;
```
Python call stacks are not being unwound after calling `emscripten_set_main_loop()`.
see: [pyodide#3697](https://github.com/pyodide/pyodide/issues/3697)

28
packages/pyxel/meta.yaml Normal file
View File

@ -0,0 +1,28 @@
package:
name: pyxel
version: 1.9.10
source:
url: https://github.com/kitao/pyxel/archive/refs/tags/v1.9.10.tar.gz
sha256: e23a0c52daaa9c4967c402b3ad2c623f33c4a01e36c4b37b884d7b8c726521f4
build:
script: |
rustup toolchain install ${RUST_TOOLCHAIN} && rustup default ${RUST_TOOLCHAIN}
rustup target add wasm32-unknown-emscripten --toolchain ${RUST_TOOLCHAIN}
embuilder build sdl2 --pic
export RUSTFLAGS="\
-C link-arg=-sUSE_SDL=2 \
-C link-arg=-fPIC \
-C link-arg=-lSDL2 \
"
requirements:
executable:
- rustup
test:
imports:
- pyxel
about:
home: https://github.com/kitao/pyxel
PyPI: https://pypi.org/project/pyxel/
summary: A retro game engine for Python

View File

@ -0,0 +1,62 @@
import pytest
@pytest.fixture(scope="function")
def selenium_sdl(selenium_standalone):
if selenium_standalone.browser == "node":
pytest.skip("No document object")
selenium_standalone.run_js(
"""
var sdl2Canvas = document.createElement("canvas");
sdl2Canvas.id = "canvas";
document.body.appendChild(sdl2Canvas);
// Temporary workaround for pyodide#3697
pyodide._api._skip_unwind_fatal_error = true;
pyodide.canvas.setCanvas2D(sdl2Canvas);
"""
)
selenium_standalone.load_package("pyxel")
yield selenium_standalone
@pytest.mark.skip_refcount_check
@pytest.mark.skip_pyproxy_check
def test_show(selenium_sdl):
selenium_sdl.run(
"""
import pyxel
pyxel.init(120, 120)
pyxel.cls(1)
pyxel.circb(60, 60, 40, 7)
pyxel.show()
"""
)
@pytest.mark.skip_refcount_check
@pytest.mark.skip_pyproxy_check
def test_run(selenium_sdl):
selenium_sdl.run(
"""
import time
import pyxel
pyxel.init(160, 120)
st = time.time()
def update():
cur = time.time()
if cur - st > 2:
pyxel.quit()
def draw():
pyxel.cls(0)
pyxel.rect(10, 10, 20, 20, 11)
pyxel.run(update, draw)
"""
)

View File

@ -78,6 +78,7 @@ API.fatal_error = function (e: any) {
if (e && e.pyodide_fatal_error) {
return;
}
if (fatal_error_occurred) {
console.error("Recursive call to fatal_error. Inner error was:");
console.error(e);
@ -138,6 +139,27 @@ API.fatal_error = function (e: any) {
throw e;
};
/**
* Signal a fatal error if the exception is not an expected exception.
*
* @argument e {any} The cause of the fatal error.
* @private
*/
API.maybe_fatal_error = function (e: any) {
// Emscripten throws "unwind" to stop current code and return to the main event loop.
// This is expected behavior and should not be treated as a fatal error.
// However, after the "unwind" exception is caught, the call stack is not unwound
// properly and there are dead frames remaining on the stack.
// This might cause problems in the future, so we need to find a way to fix it.
// See: 1) https://github.com/emscripten-core/emscripten/issues/16071
// 2) https://github.com/kitao/pyxel/issues/418
if (e && e == "unwind") {
return;
}
return API.fatal_error(e);
};
let stderr_chars: number[] = [];
API.capture_stderr = function () {
stderr_chars = [];

View File

@ -414,7 +414,12 @@ Module.callPyObjectKwargs = function (
);
Py_EXIT();
} catch (e) {
API.fatal_error(e);
if (API._skip_unwind_fatal_error) {
API.maybe_fatal_error(e);
} else {
API.fatal_error(e);
}
return;
} finally {
Hiwire.decref(idargs);
Hiwire.decref(idkwnames);

View File

@ -3,6 +3,7 @@ declare var Hiwire: any;
declare var API: any;
import "./module";
import { ffi } from "./ffi";
import { CanvasInterface, canvas } from "./canvas";
import { loadPackage, loadedPackages } from "./load-package";
import { PyBufferView, PyBuffer, TypedArray, PyProxy } from "./pyproxy.gen";
@ -121,6 +122,14 @@ export class PyodideAPI {
*/
static PATH = {} as any;
/**
* This provides APIs to set a canvas for rendering graphics.
*
* For example, you need to set a canvas if you want to use the
* SDL library. See :ref:`using-sdl` for more information.
*/
static canvas: CanvasInterface = canvas;
/**
* A map from posix error names to error codes.
*/

68
src/js/canvas.ts Normal file
View File

@ -0,0 +1,68 @@
declare var Module: any;
/**
* This interface contains the helper functions for using the HTML5 canvas.
*
* As of now, Emscripten uses Module.canvas to get the canvas element.
* This might change in the future, so we abstract it here.
* @private
*/
export interface CanvasInterface {
setCanvas2D(canvas: HTMLCanvasElement): void;
getCanvas2D(): HTMLCanvasElement | undefined;
setCanvas3D(canvas: HTMLCanvasElement): void;
getCanvas3D(): HTMLCanvasElement | undefined;
}
// We define methods here to make sphinx-js generate documentation for them.
/**
* @param canvas The HTML5 canvas element to use for 2D rendering. For now,
* Emscripten only supports one canvas element, so setCanvas2D and setCanvas3D
* are the same.
*/
export const setCanvas2D = (canvas: HTMLCanvasElement) => {
if (canvas.id !== "canvas") {
console.warn(
"If you are using canvas element for SDL library, it should have id 'canvas' to work properly.",
);
}
Module.canvas = canvas;
};
/**
*
* @returns The HTML5 canvas element used for 2D rendering. For now,
* Emscripten only supports one canvas element, so getCanvas2D and getCanvas3D
* are the same.
*/
export const getCanvas2D = (): HTMLCanvasElement | undefined => {
return Module.canvas;
};
/**
* @param canvas The HTML5 canvas element to use for 3D rendering. For now,
* Emscripten only supports one canvas element, so setCanvas2D and setCanvas3D
* are the same.
*/
export const setCanvas3D = (canvas: HTMLCanvasElement) => {
setCanvas2D(canvas);
};
/**
*
* @returns The HTML5 canvas element used for 3D rendering. For now,
* Emscripten only supports one canvas element, so getCanvas2D and getCanvas3D
* are the same.
*/
export const getCanvas3D = (): HTMLCanvasElement | undefined => {
return getCanvas2D();
};
/**
* @private
*/
export const canvas: CanvasInterface = {
setCanvas2D,
getCanvas2D,
setCanvas3D,
getCanvas3D,
};

View File

@ -55,6 +55,7 @@ export interface Module {
PATH: any;
TTY: any;
FS: FS;
canvas?: HTMLCanvasElement;
addRunDependency: (id: string) => void;
removeRunDependency: (id: string) => void;
}

37
src/tests/test_canvas.py Normal file
View File

@ -0,0 +1,37 @@
import pytest
@pytest.mark.xfail_browsers(node="No document object")
def test_canvas2D(selenium_standalone):
selenium_standalone.run_js(
"""
const canvas = document.createElement('canvas');
canvas.id = "canvas";
// Temporary workaround for pyodide#3697
pyodide._api._skip_unwind_fatal_error = true;
pyodide.canvas.setCanvas2D(canvas);
assert(() => pyodide._module.canvas === canvas);
assert(() => pyodide.canvas.getCanvas2D() === canvas);
"""
)
@pytest.mark.xfail_browsers(node="No document object")
def test_canvas3D(selenium_standalone):
selenium_standalone.run_js(
"""
const canvas = document.createElement('canvas');
canvas.id = "canvas";
// Temporary workaround for pyodide#3697
pyodide._api._skip_unwind_fatal_error = true;
pyodide.canvas.setCanvas3D(canvas);
assert(() => pyodide._module.canvas === canvas);
assert(() => pyodide.canvas.getCanvas3D() === canvas);
"""
)