mirror of https://github.com/pyodide/pyodide.git
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:
parent
7f4f66b34b
commit
2046310460
7
Makefile
7
Makefile
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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`.
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -189,4 +189,5 @@ main();
|
|||
:hidden:
|
||||
|
||||
packages-in-pyodide.md
|
||||
sdl.md
|
||||
```
|
||||
|
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
||||
"""
|
||||
)
|
|
@ -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 = [];
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -55,6 +55,7 @@ export interface Module {
|
|||
PATH: any;
|
||||
TTY: any;
|
||||
FS: FS;
|
||||
canvas?: HTMLCanvasElement;
|
||||
addRunDependency: (id: string) => void;
|
||||
removeRunDependency: (id: string) => void;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
"""
|
||||
)
|
Loading…
Reference in New Issue