Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Emscripten Working Group #7168

ofTheo started this conversation in General
Nov 16, 2022 · 39 comments · 33 replies
Discussion options

Wanted to create this discussion to sync up some of the different emscripten efforts.

@Jonathhhan has been working on a bunch of fixes but I think we need a bit of work to get things integrated cleanly.
@themancalledjakob has also been working on emscripten fixes

There are quite a lot of open emscripten issues:
https://github.com/openframeworks/openFrameworks/issues?q=is%3Aissue+is%3Aopen+Emscripten

I think the short term goal should be to get emscripten on parity with the desktop releases ( ie: fixing obvious bugs and missing features ).

Like:
#6758
#6789

Currently there is a massive PR from @Jonathhhan that is hard to merge.
It would be amazing if we could do bite size PRs for the individual issues with fixes cherry picked if needed.

Once the major issues are fixed, would be happy to continue to think about how we could improve the OF emscripten tools.

@dimitre @Jonathhhan @themancalledjakob is there anyone else we should tag for this discussion?

Thanks so much!
Theo

You must be logged in to vote

Replies: 39 comments 33 replies

Comment options

@Jonathhhan actually called me a while ago and we discussed cleaning up the PR by splitting it into bite size PRs. it takes me quite long to cleanly do this though, because i don't have that much experience and need to double/tripple check every little step and am frequently distracted by work, but i am very happy to be involved here.

would you prefer if the PRs directly target master, or would you rather have a testing branch to collect the PRs and later merge them in master together?

You must be logged in to vote
0 replies
Comment options

Here is a pull request, only with the necessary changes: https://github.com/Jonathhhan/openFrameworks/tree/emscripten_3.1.19 (should also work with current Emscripten). Still struggeling with Emscriptens current audioWorklet implementation (which is not included in this pull request). My problem with the current audioWorklet implementation is a mutex issue, that I have only with ofxOfelia (but it did not happen with the inofficial implementation). I have some additional ideas that are not included in this pull request (they already work, but could be optimized), like webMIDI, audio/video lib improvements or some filesystem stuff (like loading local files into Emscripten).

Here is the current audioWorklet discussion (maybe someone has an idea?): emscripten-core/emscripten#16449

You must be logged in to vote
0 replies
Comment options

ofTheo
Nov 17, 2022
Maintainer Author

Thanks @Jonathhhan @themancalledjakob - awesome you are working together on this.

I think the PR ( #7056 ) could be merged once it is passing the CI tests.
I am guessing the Freetype apothecary fix is needed.

I'll ask some questions in the PR.
Going forward I think bite sized PRs to master would be preferred as it is easier to review and merge them that way.

Also happy to look at the audioWorklet stuff once we get the current PR merged in.

You must be logged in to vote
0 replies
Comment options

I made a list with examples that do not work or have some issues:

assimpExample - model 2 and 5 have shader errors
cameraRibbonExample - stops when right or middle mouse button is pressed
all communication examples - i guess, they can not work with emscripten
kinectExample - error compiling kinectExample (fatal error: 'windows.h' file not found - i guess, it can not work with emscripten)
computeShaderParticlesExample - error compiling computeShaderParticlesExample (opengl 440 - error: use of undeclared identifier 'GL_COMPUTE_SHADER')
computeShaderTextureExample - error compiling computeShaderTextureExample ('ofxCvFloatImage::setRoiFromPixels' hides overloaded virtual function)
geometryShaderExample - shader does not work (but without errors) - Neither WebGL nor WebGL2 support geometry shaders
gpuParticleSystemExample - error compiling gpuParticleSystemExample (error: use of undeclared identifier 'GL_RGB32F')
multiTextureShaderExample - shader error - should be solvable
pixelBufferExample - error compiling pixelBufferExample
shadowsExample - without shadows
pointAsTexturesExample - ofEnablePointSprites() seems to have no effect
slowFastRenderingExample - error compiling slowFastRenderingExample
textureBufferInstancedExample - error compiling textureBufferInstancedExample
threadedPixelBufferExample - error compiling threadedPixelBufferExample
transformFeedbackAnimatedExample - error compiling transformFeedbackAnimatedExample
transformFeedbackExample - error compiling transformFeedbackExample
vboMeshDrawInstancedExample - shader error - should be solvable
colorExample - very slow
polylineBlobsExample - error compiling polylineBlobsExample
gui examples with osc - i guess, they can not work
parameterGroupExample - should be solvable?
clipboardExample - can not work
dragDropExample - can not work (drag and drop works with emscripten, but differently)
fileOpenSaveDialogExample - can not work (open and save local files works with emscripten, but differently)
imageLoaderWebExample - cors policy
pdfExample - error compiling pdfExample
systemSpeakExample - error compiling systemSpeakExample
allAddonsExample - windows.h not found (can not work)
all thread examples - i guess, they can not work
You must be logged in to vote
0 replies
Comment options

ofTheo
Nov 23, 2022
Maintainer Author

Awesome thanks for this list @Jonathhhan !
I fixed the assimp model issue - I was using the absolute file path which breaks if the path doesn't exist which is common when the model has the texture embedded in memory.

With the fix the first model works now - will do a PR.

Screen Shot 2022年11月22日 at 7 37 00 PM

Also the video grabber fixes seem to work great. 🙏👏

You must be logged in to vote
0 replies
Comment options

No luck with the thread examples, but since Emscripten supports threads, maybe it is possible...
https://emscripten.org/docs/porting/pthreads.html
Another thing (which is more realistic) that would be nice, is if the video player and grabber can get the width / height properties directly (and maybe the texture, too), so that the values can be used in ofSetup() and the texture without delay in ofUpdate() / ofDraw().
And a question regarding library_html5audio.js do we really need an array of audioContexts or is one enough?
This is how I implemented it some time ago: https://github.com/Jonathhhan/openFrameworks/blob/AudioWorklet/addons/ofxEmscripten/libs/html5audio/lib/emscripten/library_html5audioWorklet.js
I also added those 2 methods:

 html5audio_sound_set_pan: function (sound_id, pan) {
 	AUDIO.soundPans[sound_id].pan.value = pan;
 },
 html5audio_sound_pan: function (sound_id) {
 return AUDIO.soundPans[sound_id].pan.value;
 },
You must be logged in to vote
0 replies
Comment options

No luck with the thread examples, but since Emscripten supports threads, maybe it is possible...
https://emscripten.org/docs/porting/pthreads.html
Another thing (which is more realistic) [...]

uuh, threads would be sexy of course, and it seems as if most major browsers have support for them now https://caniuse.com/sharedarraybuffer
but you're very likely right, not sure if it's realistic for now as it requires some refactoring and special handling of browser threads here. maybe there could eventually be an ofBrowserThread to handle this.. but yes, it's probably better to focus now on the more basic stuff

do we really need an array of audioContexts or is one enough?

what was your experience with having a single one? Any downsides?

is there an oF etherpad somewhere? otherwise I put @Jonathhhan 's list in here, so we can update it as we go:
https://pads.ccc.de/Lu6s668Ruk

You must be logged in to vote
1 reply
Comment options

do we really need an array of audioContexts or is one enough?

what was your experience with having a single one? Any downsides?

@themancalledjakob no, I did not experienced any downsides and in my logic one is enough. But I am not sure.

And -s USE_PTHREADS actually works with OF (needed for audioWorklets) but not ofThreads (I could compile the threaded image loader example after some edits, but still not working...).
And thank you for the pad and the web thing, that is both very useful.

Comment options

In this example I implemented webMIDI and it is possible to load and save MIDI-files: https://midifilemarkovb.handmadeproductions.de/
https://github.com/Jonathhhan/ofEmscriptenExamples/tree/main/emscriptenMidifileMarkovB
https://github.com/Jonathhhan/openFrameworks/blob/AudioWorklet/addons/ofxEmscripten/libs/html5audio/lib/emscripten/library_webMidi.js
It would be very nice to implement webMIDI properly... (maybe choose the MIDI-device from the OF canvas - same with cam and audio devices?)

You must be logged in to vote
0 replies
Comment options

Here is a draft for having only one audioContext: https://github.com/Jonathhhan/openFrameworks/tree/one_audioContext

You must be logged in to vote
0 replies
Comment options

Here is my second fix for ofxEmscriptenVideoPlayer. Need to test it more, before I make a pull request.
But maybe a good start for a discussion:
https://github.com/Jonathhhan/openFrameworks/tree/ofxEmscriptenVideoPlayer-pan
@ofTheo maybe still too much changes for one pull request?
Edit: Also opened a discussion on the Emscripten page: emscripten-core/emscripten#18262

You must be logged in to vote
3 replies
Comment options

ofTheo Nov 30, 2022
Maintainer Author

@Jonathhhan those changes look pretty good actually.
Is RGBA faster than RGB?

Comment options

@ofTheo Yes, I guess so. From the forum: https://forum.openframeworks.cc/t/ofxemscriptenvideoplayer-optimization/39015/5

I found a way to optimize Emscripten video a bit (about 50 %):
With setUsePixels(false); and setPixelFormat(OF_PIXELS_RGBA); used together.
setUsePixels() works only with an ofxEmscriptenVideoPlayer instance (not with ofVideoPlayer).
And draw it with:
if (videoPlayer.getTexture() -> isAllocated()) {
videoPlayer.getTexture() -> draw(50, 350, 205, 110);
}

And because of this (I guess, thats related):

if (video.pixelFormat=="RGBA"){
//TODO: this is faster but under chrome, loop and set_time stop working
//array.set(imageData.data);

(It works for me on Chrome without any problems, but maybe needs some more tests...)
Not sure about the videoGrabber (RGB is the default now).

Comment options

Here is a new (more complete) PR: #7217 (I closed it, because I was unsure...)
At least it would be possible to seperate it into audio / video / grabber PRs.

Comment options

Here I found an interesting graphics project, that uses Emscripten, too: https://floooh.github.io/sokol-html5/index.html
And there it seems shadows are possible with GLES (in contrast to what the OF shadows example is saying): https://floooh.github.io/sokol-html5/shadows-sapp.html

You must be logged in to vote
6 replies
Comment options

Hi @Jonathhhan,
Shadows are definitely possible with GLES, they are just currently not supported in OF. I am focusing on some PBR stuff right now, but would love to get shadows in GLES!

Comment options

Loving all of the emscripten work! OF on the web is absolutely amazing :)

Comment options

@ofTheo the thing is, GLES3 (which is specified with #version 300 es) does not work on all mobile devices (but on most, like ~90%).
https://www.reddit.com/r/godot/comments/rc1hkq/2d_what_would_make_you_choose_gles2_over_gles3/
I would say its universal with TARGET_OPENGL_ES, but the best thing would be, if someone developing for Android / iOs can verify that.
Another thing is, some of the examples could not work with GLES2 (because some features are missing).

Comment options

ofTheo Dec 2, 2022
Maintainer Author

Thanks @Jonathhhan - I'll maybe ask @danoli3 @danzeeeman chime in about GLES3. I was thinking that maybe we could default to it on and then you could aways specify 2 or 1 via the window settings?

Comment options

@ofTheo that sounds good. Maybe -s WEBGL2_BACKWARDS_COMPATIBILITY_EMULATION=1 needs to be added to config.emscripten.default.mk then.

Comment options

This is a way to get a list of the connected devices (works with Chrome, Firefox has some issues):

{
 navigator.mediaDevices.enumerateDevices()
 .then ( function (devices) {
 console.log(devices)
 const videoDevices = devices.filter(device => device.kind === 'videoinput')
 console.log(videoDevices)
 })
}

But no idea, how to select a device from that list...

You must be logged in to vote
4 replies
Comment options

enumerateDevices should give you an id that you can use with navigator.mediaDevices.getUserMedia.

does this help? https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia

relevant snippet from there:
// Another non-number constraint is the deviceId constraint.
// If you have a deviceId from mediaDevices.enumerateDevices(), you can use it to request a specific device:

getUserMedia({
 video: {
 deviceId: myPreferredCameraDeviceId
 }
})
Comment options

@themancalledjakob thanks, I guess that helps :) Yesterday I made a PR that prints the devices in Java Script, just not sure how to pass the result back to OF. #7226

Comment options

ofTheo Dec 2, 2022
Maintainer Author

This PR looks good @Jonathhhan - I think just being able to list the devices is really helpful for now.
I am not sure exactly how you would go from a JS array of strings to a C style arr of strings, but I am guessing that is what you would need to do.

Or as you mentioned you could pack it all to a single string and then unpack it on the OF end?

eg: "1-Built In Webcam[,]2-Logitech C920[,]3-UVC Capture Card"

then in C++/OF auto vectorOfDevices = ofSplitString(listedDevices, "[,]");

I might merge the PR as is, but feel free to add the returning of the devices as another PR if you can figure it out 🙂

Comment options

@ofTheo I was wrong, sending a string is actually quite easy, its also already done in the addon:
return allocate(intArrayFromString(VIDEO.grabbers[id].pixelFormat), ALLOC_STACK);
The problem is, that the device list loads async so ofxEmscriptenVideoGrabber::listDevices() can not return the result.
There is a way with embind to get async data to OF, it looks like that (https://github.com/Jonathhhan/openFrameworks/tree/embind-example):

ofEvent<std::string> videoDevicesEvent;
ofxEmscriptenVideoGrabber::ofxEmscriptenVideoGrabber()
:id(html5video_grabber_create())
,desiredFramerate(-1)
,usePixels(true){
	ofAddListener(videoDevicesEvent, this, &ofxEmscriptenVideoGrabber::videoDevices2);
}
// load async strings from JS
vector<ofVideoDevice> ofxEmscriptenVideoGrabber::listDevices() const{
	html5video_list_devices(); // calls JS
	return vector<ofVideoDevice>(); // does nothing
}
void videoDevices1(std::string videoDevices){
	videoDevicesEvent.notify(videoDevices); // called from JS, sends it into the addons scope
}
void ofxEmscriptenVideoGrabber::videoDevices2(std::string &videoDevices) {
 ofLog(OF_LOG_NOTICE, "device list is loaded");
	std::vector<std::string> deviceList = ofSplitString(videoDevices, ",", true);
	for (auto device : deviceList) {
 ofLog(OF_LOG_NOTICE, device);
 }
}
EMSCRIPTEN_BINDINGS(Module) {
	emscripten::function("videoDevices1", &videoDevices1); // defines the embind method
}

html5video_list_devices() returns the string with Module.videoDevices1(string).
Also it seems not possible to give ofxEmscriptenVideoGrabber::listDevices() a different return type than std::vector<ofVideoDevice>.
Maybe there is a much easier and better solution, but thats the only way I found to send async data to OF (I send paths from local files the same way).

Comment options

With some very small changes it was possible to make ofxImGui work: https://ofximgui.handmadeproductions.de/
https://github.com/Jonathhhan/ofxImGui/tree/Emscripten-fix

You must be logged in to vote
0 replies
Comment options

For the svgExample I need the libxml2 lib in the lib folder (which does not seem to be installed by default anymore).

You must be logged in to vote
0 replies
Comment options

I simplified loading local files (compared to how I did that before).
One example is with the new FilePicker, which is very nice but does not work with Firefox.

https://github.com/Jonathhhan/ofEmscriptenExamples/tree/main/emscriptenImport_Em_Async_JS_FilePicker
https://developer.mozilla.org/en-US/docs/Web/API/Window/showOpenFilePicker

The other example uses input and should work on all browsers (in that sense it is better).
https://github.com/Jonathhhan/ofEmscriptenExamples/tree/main/emscriptenImport_Em_Async_JS

https://import.handmadeproductions.de/ (that is the old version)

Maybe someone can have a look at those methods and maybe optimize them (and do not now Java Script well, and it is possible that I just miss something)? There is still one issue, that is the GUI freezes, if the file browser is discarded. And it unfreezes if the next file is selected (I guess it is, because the methods are not resolved, if no file is selected).

The old approach with embind still has some advantages, like not stopping the video while opening the browser, but is much more complicated to implemernt: https://github.com/Jonathhhan/ofEmscriptenExamples/tree/main/emscriptenImport

And I had the idea to implement this: https://github.com/GoogleChromeLabs/browser-fs-access
It could make the use of the FS much easier, but no idea how to implement it. And also not sure, if it makes sense.

Another finding is, that apps without ofRunApp(window, make_shared<ofApp>()); have errors in debug mode, and some do not work at all because of that.

You must be logged in to vote
0 replies
Comment options

Hey @ofTheo,
no worries and thanks. Actually most of my pull requests where I do not have any questions are merged.
Those are left (you need to decide what to merge in the end, of course):
#7211
#7214

The other prs could need some discussion / changes / improvements:
webMidi works quite well, but maybe need an example to show how to use it:
#7259
Select audio (in) devices works well, not sure if it should be a seperate file:
#7261
Gamepad event works well, but could be fine tuned. There is also the Emscripten event array, that could simplify other Emscripten events too:
#7244
Focus / blur can be set directly in ofApp.cpp, so this pr is not needed anymore:
#7237
Here is one issue with canvas and mouse events:
#7272
And finally the audioWorklet stuff, which is not a pr yet because it needs a better implementation:
#7260

I guess thats it from my side.

You must be logged in to vote
0 replies
Comment options

It seems Emscriptens audioWorklet support will be merged into their main branch soon, I think it could be quite a big improvement to implement them in OF too... emscripten-core/emscripten#16449

You must be logged in to vote
0 replies
Comment options

Since yesterday audioWorklets are supported by Emscriptens main branch.
I already have an (older) implementation, but that is quite hacky and I needed to edit some Emscripten files.
I am sure it is possibly to implement without editing any Emscripten files, just no idea how to do that.
Here is an official example: https://github.com/emscripten-core/emscripten/blob/main/test/webaudio/tone_generator.c

You must be logged in to vote
0 replies
Comment options

The reason that compute and geometry shaders do not work with Emscripten is (at the moment), that Emscripten uses webGL2 which is based on GL ES 3.0. Compute shaders were introduced in GL ES 3.1 and geometry shaders in GL ES 3.2, so it does not seem very likely that the they will work soon (maybe and hopefully I am wrong).
https://en.wikipedia.org/wiki/OpenGL_ES
https://stackoverflow.com/questions/70483149/what-are-the-latest-versions-of-opengl-es-supported-by-emscripten-and-how-do-i

Same for textureBufferInstancedExample because sampleBuffer is only available from 3.1 on: https://registry.khronos.org/OpenGL/extensions/EXT/EXT_texture_buffer.txt
Right now I get the error: 'samplerBuffer' : Illegal use of reserved word

And with vboMeshDrawInstancedExample I get (after enabling vaoSupported = glGenVertexArrays in ofVbo::bind()):
Aborted(To use dlopen, you need enable dynamic linking, see https://github.com/emscripten-core/emscripten/wiki/Linking)

You must be logged in to vote
0 replies
Comment options

You must be logged in to vote
0 replies
Comment options

@Jonathhhan I saw the multiTextureShaderExample listed in the pad, would you like me to update that one for Emscripten?

You must be logged in to vote
1 reply
Comment options

@NickHardeman of course, that would be great :)
I already worked on the other 3 missing examples (textureBufferInstancedExample, transformFeedbackExample and vboMeshDrawInstancedExample) but still have some issues. Will give it a second try and / or post the detailed error messages here.

Comment options

@Jonathhhan submitted #7323 for the multiTextureShaderExample :)

You must be logged in to vote
1 reply
Comment options

@NickHardeman thank you. The reason that the video does not start to play is that it needs a user interaction, like audio too. The materialPBR and materialPBRAdvanced do also work, which is great.

Comment options

Just printed the glInfoExample. Here is the result (maybe helpful for checking what could work). Can also depend on the GPU, of course. And maybe some of the info is not correct, because I edited the patch wrong. There is also a list of the available extensions:

opengl info
-------------------------------------------------
version=OpenGL ES 3.0 (WebGL 2.0 (OpenGL ES 3.0 Chromium))
vendor=WebKit
renderer=WebKit WebGL
-------------------------------------------------
opengl limits
-------------------------------------------------
OpenGL limits:
 GL_MAX_ELEMENTS_VERTICES = 1048576
 GL_MAX_ELEMENTS_INDICES = 1048576
 GL_MAX_TEXTURE_SIZE = 32768
 GL_MAX_3D_TEXTURE_SIZE = 16384
 GL_MAX_CUBE_MAP_TEXTURE_SIZE_ARB = 32768
 GL_NUM_COMPRESSED_TEXTURE_FORMATS_ARB = 16
 GL_MAX_TEXTURE_LOD_BIAS_EXT = 15
 GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT = 16
 GL_MAX_VIEWPORT_DIMS = 32768, 32768
 GL_ALIASED_LINE_WIDTH_RANGE = 1, 10
 GL_ALIASED_POINT_SIZE_RANGE = 1, 2047
-------------------------------------------------
shader limits
-------------------------------------------------
Shader limits:
 GL_MAX_VERTEX_ATTRIBS = 16
 GL_MAX_VERTEX_UNIFORM_COMPONENTS = 4096
 GL_MAX_VARYING_FLOATS = 124
 GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS = 32
 GL_MAX_TEXTURE_IMAGE_UNITS = 32
-------------------------------------------------
available extensions
-------------------------------------------------
 EXT_color_buffer_float, EXT_color_buffer_half_float, 
 EXT_disjoint_timer_query_webgl2, EXT_float_blend, 
 EXT_texture_compression_bptc, EXT_texture_compression_rgtc, 
 EXT_texture_filter_anisotropic, EXT_texture_norm16, 
 KHR_parallel_shader_compile, OES_draw_buffers_indexed, 
 OES_texture_float_linear, WEBGL_compressed_texture_s3tc, 
 WEBGL_compressed_texture_s3tc_srgb, WEBGL_debug_renderer_info, 
 WEBGL_debug_shaders, WEBGL_lose_context, WEBGL_multi_draw, 
 OVR_multiview2, GL_EXT_color_buffer_float, GL_EXT_color_buffer_half_float, 
 GL_EXT_disjoint_timer_query_webgl2, GL_EXT_float_blend, 
 GL_EXT_texture_compression_bptc, GL_EXT_texture_compression_rgtc, 
 GL_EXT_texture_filter_anisotropic, GL_EXT_texture_norm16, 
 GL_KHR_parallel_shader_compile, GL_OES_draw_buffers_indexed, 
 GL_OES_texture_float_linear, GL_WEBGL_compressed_texture_s3tc, 
 GL_WEBGL_compressed_texture_s3tc_srgb, GL_WEBGL_debug_renderer_info, 
 GL_WEBGL_debug_shaders, GL_WEBGL_lose_context, GL_WEBGL_multi_draw, 
 GL_OVR_multiview2
-------------------------------------------------
opengl calls available
-------------------------------------------------
GL_VERSION_1_1: OK 
---------------
GL_VERSION_1_2: OK 
---------------
 glCopyTexSubImage3D: OK
 glDrawRangeElements: OK
 glTexImage3D: OK
 glTexSubImage3D: OK
GL_VERSION_1_3: OK 
---------------
 glActiveTexture: OK
 glClientActiveTexture: OK
 glCompressedTexImage1D: OK
 glCompressedTexImage2D: OK
 glCompressedTexImage3D: OK
 glCompressedTexSubImage1D: OK
 glCompressedTexSubImage2D: OK
 glCompressedTexSubImage3D: OK
 glGetCompressedTexImage: OK
 glLoadTransposeMatrixd: OK
 glLoadTransposeMatrixf: OK
 glMultTransposeMatrixd: OK
 glMultTransposeMatrixf: OK
 glMultiTexCoord1d: OK
 glMultiTexCoord1dv: OK
 glMultiTexCoord1f: OK
 glMultiTexCoord1fv: OK
 glMultiTexCoord1i: OK
 glMultiTexCoord1iv: OK
 glMultiTexCoord1s: OK
 glMultiTexCoord1sv: OK
 glMultiTexCoord2d: OK
 glMultiTexCoord2dv: OK
 glMultiTexCoord2f: OK
 glMultiTexCoord2fv: OK
 glMultiTexCoord2i: OK
 glMultiTexCoord2iv: OK
 glMultiTexCoord2s: OK
 glMultiTexCoord2sv: OK
 glMultiTexCoord3d: OK
 glMultiTexCoord3dv: OK
 glMultiTexCoord3f: OK
 glMultiTexCoord3fv: OK
 glMultiTexCoord3i: OK
 glMultiTexCoord3iv: OK
 glMultiTexCoord3s: OK
 glMultiTexCoord3sv: OK
 glMultiTexCoord4d: OK
 glMultiTexCoord4dv: OK
 glMultiTexCoord4f: OK
 glMultiTexCoord4fv: OK
 glMultiTexCoord4i: OK
 glMultiTexCoord4iv: OK
 glMultiTexCoord4s: OK
 glMultiTexCoord4sv: OK
 glSampleCoverage: OK
GL_VERSION_1_4: OK 
---------------
 glBlendColor: OK
 glBlendEquation: OK
 glBlendFuncSeparate: OK
 glFogCoordPointer: OK
 glFogCoordd: OK
 glFogCoorddv: OK
 glFogCoordf: OK
 glFogCoordfv: OK
 glMultiDrawArrays: OK
 glMultiDrawElements: OK
 glPointParameterf: OK
 glPointParameterfv: OK
 glPointParameteri: OK
 glPointParameteriv: OK
 glSecondaryColor3b: OK
 glSecondaryColor3bv: OK
 glSecondaryColor3d: OK
 glSecondaryColor3dv: OK
 glSecondaryColor3f: OK
 glSecondaryColor3fv: OK
 glSecondaryColor3i: OK
 glSecondaryColor3iv: OK
 glSecondaryColor3s: OK
 glSecondaryColor3sv: OK
 glSecondaryColor3ub: OK
 glSecondaryColor3ubv: OK
 glSecondaryColor3ui: OK
 glSecondaryColor3uiv: OK
 glSecondaryColor3us: OK
 glSecondaryColor3usv: OK
 glSecondaryColorPointer: OK
 glWindowPos2d: OK
 glWindowPos2dv: OK
 glWindowPos2f: OK
 glWindowPos2fv: OK
 glWindowPos2i: OK
 glWindowPos2iv: OK
 glWindowPos2s: OK
 glWindowPos2sv: OK
 glWindowPos3d: OK
 glWindowPos3dv: OK
 glWindowPos3f: OK
 glWindowPos3fv: OK
 glWindowPos3i: OK
 glWindowPos3iv: OK
 glWindowPos3s: OK
 glWindowPos3sv: OK
GL_VERSION_1_5: OK 
---------------
 glBeginQuery: OK
 glBindBuffer: OK
 glBufferData: OK
 glBufferSubData: OK
 glDeleteBuffers: OK
 glDeleteQueries: OK
 glEndQuery: OK
 glGenBuffers: OK
 glGenQueries: OK
 glGetBufferParameteriv: OK
 glGetBufferPointerv: OK
 glGetBufferSubData: OK
 glGetQueryObjectiv: OK
 glGetQueryObjectuiv: OK
 glGetQueryiv: OK
 glIsBuffer: OK
 glIsQuery: OK
 glMapBuffer: OK
 glUnmapBuffer: OK
GL_VERSION_2_0: OK 
---------------
 glAttachShader: OK
 glBindAttribLocation: OK
 glBlendEquationSeparate: OK
 glCompileShader: OK
 glCreateProgram: OK
 glCreateShader: OK
 glDeleteProgram: OK
 glDeleteShader: OK
 glDetachShader: OK
 glDisableVertexAttribArray: OK
 glDrawBuffers: OK
 glEnableVertexAttribArray: OK
 glGetActiveAttrib: OK
 glGetActiveUniform: OK
 glGetAttachedShaders: OK
 glGetAttribLocation: OK
 glGetProgramInfoLog: OK
 glGetProgramiv: OK
 glGetShaderInfoLog: OK
 glGetShaderSource: OK
 glGetShaderiv: OK
 glGetUniformLocation: OK
 glGetUniformfv: OK
 glGetUniformiv: OK
 glGetVertexAttribPointerv: OK
 glGetVertexAttribdv: OK
 glGetVertexAttribfv: OK
 glGetVertexAttribiv: OK
 glIsProgram: OK
 glIsShader: OK
 glLinkProgram: OK
 glShaderSource: OK
 glStencilFuncSeparate: OK
 glStencilMaskSeparate: OK
 glStencilOpSeparate: OK
 glUniform1f: OK
 glUniform1fv: OK
 glUniform1i: OK
 glUniform1iv: OK
 glUniform2f: OK
 glUniform2fv: OK
 glUniform2i: OK
 glUniform2iv: OK
 glUniform3f: OK
 glUniform3fv: OK
 glUniform3i: OK
 glUniform3iv: OK
 glUniform4f: OK
 glUniform4fv: OK
 glUniform4i: OK
 glUniform4iv: OK
 glUniformMatrix2fv: OK
 glUniformMatrix3fv: OK
 glUniformMatrix4fv: OK
 glUseProgram: OK
 glValidateProgram: OK
 glVertexAttrib1d: OK
 glVertexAttrib1dv: OK
 glVertexAttrib1f: OK
 glVertexAttrib1fv: OK
 glVertexAttrib1s: OK
 glVertexAttrib1sv: OK
 glVertexAttrib2d: OK
 glVertexAttrib2dv: OK
 glVertexAttrib2f: OK
 glVertexAttrib2fv: OK
 glVertexAttrib2s: OK
 glVertexAttrib2sv: OK
 glVertexAttrib3d: OK
 glVertexAttrib3dv: OK
 glVertexAttrib3f: OK
 glVertexAttrib3fv: OK
 glVertexAttrib3s: OK
 glVertexAttrib3sv: OK
 glVertexAttrib4Nbv: OK
 glVertexAttrib4Niv: OK
 glVertexAttrib4Nsv: OK
 glVertexAttrib4Nub: OK
 glVertexAttrib4Nubv: OK
 glVertexAttrib4Nuiv: OK
 glVertexAttrib4Nusv: OK
 glVertexAttrib4bv: OK
 glVertexAttrib4d: OK
 glVertexAttrib4dv: OK
 glVertexAttrib4f: OK
 glVertexAttrib4fv: OK
 glVertexAttrib4iv: OK
 glVertexAttrib4s: OK
 glVertexAttrib4sv: OK
 glVertexAttrib4ubv: OK
 glVertexAttrib4uiv: OK
 glVertexAttrib4usv: OK
 glVertexAttribPointer: OK
GL_VERSION_2_1: OK 
---------------
 glUniformMatrix2x3fv: OK
 glUniformMatrix2x4fv: OK
 glUniformMatrix3x2fv: OK
 glUniformMatrix3x4fv: OK
 glUniformMatrix4x2fv: OK
 glUniformMatrix4x3fv: OK
GL_VERSION_3_0: MISSING 
---------------
 glBeginConditionalRender: OK
 glBeginTransformFeedback: OK
 glBindFragDataLocation: OK
 glClampColor: OK
 glClearBufferfi: OK
 glClearBufferfv: OK
 glClearBufferiv: OK
 glClearBufferuiv: OK
 glColorMaski: OK
 glDisablei: OK
 glEnablei: OK
 glEndConditionalRender: OK
 glEndTransformFeedback: OK
 glGetBooleani_v: OK
 glGetFragDataLocation: OK
 glGetStringi: OK
 glGetTexParameterIiv: OK
 glGetTexParameterIuiv: OK
 glGetTransformFeedbackVarying: OK
 glGetUniformuiv: OK
 glGetVertexAttribIiv: OK
 glGetVertexAttribIuiv: OK
 glIsEnabledi: OK
 glTexParameterIiv: OK
 glTexParameterIuiv: OK
 glTransformFeedbackVaryings: OK
 glUniform1ui: OK
 glUniform1uiv: OK
 glUniform2ui: OK
 glUniform2uiv: OK
 glUniform3ui: OK
 glUniform3uiv: OK
 glUniform4ui: OK
 glUniform4uiv: OK
 glVertexAttribI1i: OK
 glVertexAttribI1iv: OK
 glVertexAttribI1ui: OK
 glVertexAttribI1uiv: OK
 glVertexAttribI2i: OK
 glVertexAttribI2iv: OK
 glVertexAttribI2ui: OK
 glVertexAttribI2uiv: OK
 glVertexAttribI3i: OK
 glVertexAttribI3iv: OK
 glVertexAttribI3ui: OK
 glVertexAttribI3uiv: OK
 glVertexAttribI4bv: OK
 glVertexAttribI4i: OK
 glVertexAttribI4iv: OK
 glVertexAttribI4sv: OK
 glVertexAttribI4ubv: OK
 glVertexAttribI4ui: OK
 glVertexAttribI4uiv: OK
 glVertexAttribI4usv: OK
 glVertexAttribIPointer: OK
GL_VERSION_3_1: MISSING 
---------------
 glDrawArraysInstanced: OK
 glDrawElementsInstanced: OK
 glPrimitiveRestartIndex: OK
 glTexBuffer: OK
GL_VERSION_3_2: MISSING 
---------------
 glFramebufferTexture: OK
 glGetBufferParameteri64v: OK
 glGetInteger64i_v: OK
GL_VERSION_3_3: MISSING 
---------------
 glVertexAttribDivisor: OK
GL_VERSION_4_0: MISSING 
---------------
 glBlendEquationSeparatei: OK
 glBlendEquationi: OK
 glBlendFuncSeparatei: OK
 glBlendFunci: OK
 glMinSampleShading: OK
GL_VERSION_4_1: MISSING 
---------------
GL_VERSION_4_2: MISSING 
---------------
You must be logged in to vote
0 replies
Comment options

A drawback from multi-threading is that the js 'AnalyserNode' for audio fft does not work anymore. But I think that it is worth it, because fft can be generated in OF too.
soundPlayerFFTExample.js:1 Uncaught TypeError: Failed to execute 'getFloatFrequencyData' on 'AnalyserNode': The provided Float32Array value must not be shared.

You must be logged in to vote
0 replies
Comment options

@Jonathhhan is there a way to determine if Emscripten is running on mobile?
Trying to get pbr cube maps working on iOS safari and it would be helpful

You must be logged in to vote
3 replies
Comment options

@NickHardeman I only know how to check if it is a touch device (with setting isTouch=true with the first touch input)...

Comment options

This seems to work for detecting between OSX safari and iOS safari, haven't tested on anything else.

#ifdef TARGET_EMSCRIPTEN
int mobile = EM_ASM_INT(
	if(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)){
		// true for mobile device
		return 1;
	}else{
		// false for not mobile device
		return 0;
	}
);
ofLogNotice("ofApp :: setup : mobile: ") << mobile;
#endif
Comment options

NIce, looks as if it could work for other OSs too.

Comment options

Since multithreading is working, it could also be possible to make the udp/osc examples work. At least with the oscReceive example I can receive something (the console print the correct value, but then it aborts with an error).

You must be logged in to vote
0 replies
Comment options

Hey @ofTheo,

A couple of things I noticed:

  1. error when not using emrun
  2. missing explanation how to host it (SharedArrayBuffer)
  3. copied data does not refresh every compilation
  4. gallery of examples
  5. webgl 2.0 & OpenGL ES 3.x

1) error when not using emrun

When you're not using emrun to serve the project, an error is thrown:

Failed to load resource: the server responded with a status of 404 (Not Found) :<PORT>/stdio.html:1

This error is afaik not violent, but it may be confusing or undesirable.

An easy fix would be to introduce an environment variable that you can set in your config.make on a project basis.

e.g. by default we do emrun.
if you add PROJECT_EMSCRIPTEN_NO_EMRUN = 1 -> no emrun

possible implementation:

diff --git a/libs/openFrameworksCompiled/project/emscripten/config.emscripten.default.mk b/libs/openFrameworksCompiled/project/emscripten/config.emscripten.default.mk
index cfb193bc5..c914d663e 100644
--- a/libs/openFrameworksCompiled/project/emscripten/config.emscripten.default.mk
+++ b/libs/openFrameworksCompiled/project/emscripten/config.emscripten.default.mk
@@ -93,7 +93,13 @@ ifdef USE_CCACHE
 endif
 endif
-PLATFORM_LDFLAGS = -Wl --gc-sections --preload-file bin/data@data --emrun --bind --profiling-funcs -s USE_FREETYPE=1 -s ALLOW_MEMORY_GROWTH=1 -s MAX_WEBGL_VERSION=2 -s WEBGL2_BACKWARDS_COMPATIBILITY_EMULATION=1 -s FULL_ES2 -sFULL_ES3=1 -pthread
+
+PLATFORM_LDFLAGS = -Wl --gc-sections --preload-file bin/data@data --bind --profiling-funcs -s USE_FREETYPE=1 -s ALLOW_MEMORY_GROWTH=1 -s MAX_WEBGL_VERSION=2 -s WEBGL2_BACKWARDS_COMPATIBILITY_EMULATION=1 -s FULL_ES2 -sFULL_ES3=1 -pthread
+
+ifndef PROJECT_EMSCRIPTEN_NO_EMRUN
+ PLATFORM_LDFLAGS += --emrun
+endif
+
 PLATFORM_LDFLAGS += --js-library $(OF_ADDONS_PATH)/ofxEmscripten/libs/html5video/lib/emscripten/library_html5video.js
 PLATFORM_LDFLAGS += --js-library $(OF_ADDONS_PATH)/ofxEmscripten/libs/html5audio/lib/emscripten/library_html5audio.js

2) missing explanation how to host (SharedArrayBuffer)

We need to set some headers now, it would be helpful to get advice on how to properly host us.
Godotengine lists the missing pieces: https://pointer.click/files/godot_web_errors.png
Maybe we could do something similar? Definitely it should be mentioned in the emscripten documentation how to properly do it.
Especially locally it can be tricky for beginners, as a simple php -S 0.0.0.0:9999 doesn't do the job anymore.
I currently use browser-sync for testing locally (with a bit of a hack to set the headers).
It could be great to ask how people do it. I'm sure someone has a super smart way that should be shared.

3) copied data does not refresh every compilation

I noticed, that I needed to compile a couple of times to see changes. Especially when having copied files from an addon, it takes two or even three compilation cycles to update.
This is quite frustrating, since most people will first suspect browser cache. I am currently working on an addon with shaders, there it almost drove me crazy.
I now use a simple script like this:

#!/bin/bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
PREVIOUS_DIR=$(pwd)
cd $DIR
project=$(basename $DIR)
rm -rf bin/$project*
rm -rf bin/data/ofxMsdfgen
rm -rf bin/data/ofxGPUFont
cp -r ../../../addons/ofxMsdfgen/data/ofxMsdfgen ./bin/data/
cp -r ../../../addons/ofxGPUFont/data/ofxGPUFont ./bin/data/
cd $PREVIOUS_DIR

I run this before every compilation. This cleans out all files, that need to be packaged, so that the everything updates with a single compile.
Of course this is not ideal, and would be much better if it would be handled smartly somewhere in the make-process.
I believe (could be wrong), that currently we are only checking if a file exists to determine whether it should be repacked and therefore files can be changed, but not updated. Could we hash the files and check for that? Since my stupid hack worked flawlessly I didn't look much further into this, but it's definitely something people would run into sooner or later.

4) gallery of examples

This is not really related to the next release, but a while ago I played around with the gallery of examples: https://dev.pointer.click/of/
I still think this could be great as an overview, and to show off what oF can do in the web.
I hope I'll find time soon to push this a bit further.

5) webgl 2.0 & OpenGL ES 3.x

I noticed, that opengl abilities are more limited than necessary in the preprocessor macros. For example textures can handle more webgl 2, than oF currently allows. I believe multisampled textures are such a case. I circumvented this, by writing the opengl calls directly, but it was also my first time to really deal with opengl, maybe I just missed something? While we're at this, OpenGL ES only checks for major versions. It's quite simple to add checks for minor versions as well. It's another story to adjust all preprocessor macros though. That may be a lot of work. Also, there are some things like glTexBuffer, which were introduced in ES 3.2 (https://registry.khronos.org/OpenGL-Refpages/es3/html/glTexBuffer.xhtml). I'm not sure, as far as I know it is quite impossible (is that true?) to check for this with a macro, as the version is determined in runtime.

I hope these comments are a bit clear, some of these topics are super new to me, and I hope I don't just completely misunderstand something. :)

You must be logged in to vote
3 replies
Comment options

ofTheo Apr 27, 2023
Maintainer Author

thanks for this @themancalledjakob!

  1. I have never tried to launch it any other way. Is that when you try and just open the .html?

  2. As @NickHardeman mentioned pthread is not enabled anymore in the nightly builds. You can add it by editing the config.make but we could definitely add a note on the setup guide: https://github.com/openframeworks/openFrameworks/blob/master/scripts/templates/emscripten/config.make#L2-L8

  3. This one def sounds like something we could fix - if you do export MAKEFILE_DEBUG=1 before building it should show more info including the PROJECT_ADDONS_DATA to be copied

  4. Yes - this is so awesome! We were planning on doing something similar for the main site.

  5. We would definitely be open to a PR to allow ES 3.x - this is something that could help especially with the PBR / 3D stuff @NickHardeman added recently.

Comment options

@themancalledjakob also thanks from me. If I am right, it is just needed to add -pthread at three positions in the make file. Just not sure how to document it the best way. I will test the examples later. Does it make sense to test with and without pthread support?

Comment options

thanks for this @themancalledjakob!

1. I have never tried to launch it any other way. Is that when you try and just open the .html?

no, that's not what I mean :) that won't work for other reasons.
I mean, if you want to publish your project and serve it with nginx, apache, or you want to integrate it in a local web development workflow with browser-sync or python, or similar. Basically whenever you serve without emrun you get this error. It's not critical, it's just looking for the emrun process and fails.

2. As @NickHardeman mentioned pthread is not enabled anymore in the nightly builds. You can add it by editing the config.make but we could definitely add a note on the setup guide: https://github.com/openframeworks/openFrameworks/blob/master/scripts/templates/emscripten/config.make#L2-L8

cool, I agree this sounds good.

3. This one def sounds like something we could fix - if you do `export MAKEFILE_DEBUG=1` before building it should show more info including the `PROJECT_ADDONS_DATA` to be copied

yes. I haven't yet figured out what it is. I made a quick test script to describe the issue. see below

4. Yes - this is so awesome! We were planning on doing something similar for the main site.

nice! is there already progress somewhere? Can I join the effort?

5. We would definitely be open to a PR to allow ES 3.x - this is something that could help especially with the PBR / 3D stuff @NickHardeman added recently.

cool! i'm curious how to check for features or ES version. should that be done in runtime?

Appendix for 3.

so. To better portray the issue here is a little script to create a test file with timestamp in the data folder and check at various stages where it is. If I would have a better understanding of the Make-process, I probably wouldn't need that... but maybe it helps to show you what is going wrong.

the script:

#!/bin/bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
printf -v timestamp '%(%Y-%m-%d_%H:%M:%S)T' -1
cd $DIR
project=$(basename $DIR)
# change these to match with your test project
addonsDataTestFile=ofxGPUFont/test
addon=ofxGPUFont
emsdk_sh=/home/jrkb/git/tools/emsdk/emsdk_env.sh
# info
echo "testing $project with timestamp:"
echo ""
echo $timestamp
echo ""
# create / overwrite test file
echo "copyaddonsdata: $timestamp" > ../../../addons/$addon/data/$addonsDataTestFile
# get emsdk ready
source $emsdk_sh
# compile and capture output for later analysis
echo $(emmake make) >> "0ドル_${timestamp}_emmake_make"
# check that it is properly copied
echo "timestamp in data folder:"
echo ""
cat bin/data/$addonsDataTestFile
echo ""
# make emrun fly on its own, and capture pid
nohup emrun --port 6931 --no_browser bin/$project.html < /dev/null > /dev/null 2>&1 &
pid=$!
echo "pid $pid"
# check the test file as it is served
URL="http://localhost:6931/data/$addonsDataTestFile"
is_up="no"
while [ "$is_up" == "no" ]; do
 wget_output=$(wget -q "$URL")
 if [ $? -eq 0 ]; then
 is_up="yes"
 fi
done
wget $URL
# save for later analysis
mv test "0ドル_${timestamp}_test"
# show it though
echo "timestamp downloaded:"
echo ""
cat ./0ドル_${timestamp}_test
echo ""
# and this is the part where it goes wrong
# check, how it is packed in the data
echo "timestamp packed:"
echo ""
cat bin/$project.data | grep -a "copyaddonsdata"
echo ""
# pull the trigger on emrun.. sorry
kill -9 $pid

here output (notice the last packed timestamp):

08:35:00 ~/openFrameworks/apps/lalala/thisIsAmazing
$ ./testDataCopy.sh 
testing msdf-theatre with timestamp:
2023年05月02日_08:35:00
Setting up EMSDK environment (suppress these messages with EMSDK_QUIET=1)
Setting environment variables:
make: make
timestamp in data folder:
copyaddonsdata: 2023年05月02日_08:35:00
pid 17440
--2023年05月02日 08:35:01-- http://localhost:6931/data/ofxGPUFont/test
Resolving localhost (localhost)... ::1, 127.0.0.1
Connecting to localhost (localhost)|::1|:6931... failed: Connection refused.
Connecting to localhost (localhost)|127.0.0.1|:6931... connected.
HTTP request sent, awaiting response... 200 OK
Length: 36 [application/octet-stream]
Saving to: ‘test.4’
test.4 100%[=====================================================================================================>] 36 --.-KB/s in 0s 
2023年05月02日 08:35:01 (3.28 MB/s) - ‘test.4’ saved [36/36]
timestamp downloaded:
copyaddonsdata: 2023年05月02日_08:35:00
timestamp packed:
copyaddonsdata: 2023年05月02日_08:15:25
Comment options

@themancalledjakob thank you for this write up.
For item #2, I believe pthread was causing the SharedArrayBuffer issue ( as discussed here #7412 ) Is pthread enabled in scripts/templates/emscripten/config.make ? There should also be some notes at the top, but definitely agree that there should be an explanation in the emscripten documentation.

You must be logged in to vote
0 replies
Comment options

I tested the current OF nightly with Emscripten 3.1.37 and pthread support.
These things affect the examples:
assimp3dmodel: fifth model does not load (but had issues before). instead it loads the fourth model again.
pixelBufferExample: #7314
glInfoExample: #7211
mouse position only works correctly with fullscreen if resize and / or keep aspect is selected.
filebufferloadingcsvexample only makes a sound on the first morse code (i think thats not emscripten specific).
audioinputexample receives no input (it was working before).
asciivideoexample is with webcaminput (behind the ascii), not sure if intended. it was without before.
emptyExample needs a data folder with a .gitkeep file.

These are just warnings (which maybe can be avoided, but not sure):
materialPBR gl warning:
GL_INVALID_OPERATION: Uniform size does not match uniform method.
transfornfeedbackanimatedexample: WebGL: INVALID_OPERATION: bindBufferBase: transform feedback is active
some errors with videoplayerexample (I guess, because it cannot play backwards)
opencvpoepledetection: [ error ] ofxCvImage: allocate(): width and height are zero on setup.
multitexture (shader example): 06_multiTexture.html:1 [ error ] ofTexture: getTextureData(): texture has not been allocated on setup.

You must be logged in to vote
7 replies
Comment options

ofTheo May 3, 2023
Maintainer Author

note: the above code seems to work for me in Firefox and Chrome but not Safari.

Screen Shot 2023年05月02日 at 9 03 01 PM

Comment options

@ofTheo thank you. With the current nightly audio input works again.
I just wonder why only the left side of the channel windows shows some input:
Screenshot from 2023年05月04日 04-26-37
And compiled with Linux (Ubuntu - not Emscripten related) I have some strange results:
Screenshot from 2023年05月04日 04-22-30

Comment options

ofTheo May 4, 2023
Maintainer Author

Thanks @Jonathhhan!
I didn't do a PR or anything for my changes, are you saying the current nightly works okay for you without those changes?

Maybe I need to check my emscripten version then?

The buffer thing for the first screenshot is probably an issue with the buffer size being allocated larger but then the system only using half the buffer size. Def seems like a bug.

The Ubuntu screenshot seems even worse 🤦‍♂️

@dimitre - would you be able to see if you can reproduce it?

Comment options

@ofTheo yes, without those changes and the same Emscripten version (3.1.37). Even tested with the old nightly again yesterday, and was able to reproduce the issue... maybe I will find the pr that caused the issue (or it is something else, but I did not change anything in both nightlies).

Comment options

ofTheo May 4, 2023
Maintainer Author

Thanks!
Just tried also and the current nightly works fine on Firefox and Chrome but not Safari ( on macOS ).
So safari would still be good to fix.

Comment options

We have a bunch of Open GL ES 3 support unmerged as of yet. Might be worth doing so if Emscripten web can benefit massively from it.
The gains were sub optimal on mobile, maybe worse so I never completed the merge. Did work though.

Assimp has some major updates recently. I've just merged to their master branch some fixes for us to compile latest for oF hopefully that fixes some issues when step up

You must be logged in to vote
1 reply
Comment options

ofTheo May 3, 2023
Maintainer Author

totally agree!
happy to do it maybe after 0.12.0 as that is close to being ready?
it could be something for 0.12.1 maybe?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

AltStyle によって変換されたページ (->オリジナル) /