Emscripten compatibility (#161)

* Major refactoring of the main loop(s) and control flow (WIP)

run_at_fps() is gone 🦀

Instead of nested blocking event loops, there is now an eventloop API
that manages an explicit stack of scenes. This makes Taisei a lot more
portable to async environments where spinning a loop forever without
yielding control simply is not an option, and that is the entire point
of this change.

A prime example of such an environment is the Web (via emscripten).
Taisei was able to run there through a terrible hack: inserting
emscripten_sleep calls into the loop, which would yield to the browser.
This has several major drawbacks: first of all, every function that
could possibly call emscripten_sleep must be compiled into a special
kind of bytecode, which then has to be interpreted at runtime, *much*
slower than JITed WebAssembly. And that includes *everything* down the
call stack, too! For more information, see
https://emscripten.org/docs/porting/emterpreter.html

Even though that method worked well enough for experimenting, despite
suboptimal performance, there is another obvious drawback:
emscripten_sleep is implemented via setTimeout(), which can be very
imprecise and is generally not reliable for fluid animation. Browsers
actually have an API specifically for that use case:
window.requestAnimationFrame(), but Taisei's original blocking control
flow style is simply not compatible with it. Emscripten exposes this API
with its emscripten_set_main_loop(), which the eventloop backend now
uses on that platform.

Unfortunately, C is still C, with no fancy closures or coroutines.
With blocking calls into menu/scene loops gone, the control flow is
reimplemented via so-called (pun intended) "call chains". That is
basically an euphemism for callback hell. With manual memory management
and zero type-safety. Not that the menu system wasn't shitty enough
already. I'll just keep telling myself that this is all temporary and
will be replaced with scripts in v1.4.

* improve build system for emscripten + various fixes

* squish menu bugs

* improve emscripten event loop; disable EMULATE_FUNCTION_POINTER_CASTS

Note that stock freetype does not work without
EMULATE_FUNCTION_POINTER_CASTS; use a patched version from the
"emscripten" branch here:

    https://github.com/taisei-project/freetype2/tree/emscripten

* Enable -Wcast-function-type

Calling functions through incompatible pointers is nasal demons and
doesn't work in WASM.

* webgl: workaround a crash on some browsers

* emscripten improvements:

    * Persist state (config, progress, replays, ...) in local IndexDB
    * Simpler HTML shell (temporary)
    * Enable more optimizations

* fix build if validate_glsl=false

* emscripten: improve asset packaging, with local cache

Note that even though there are rules to build audio bundles, audio
does *not* work yet. It looks like SDL2_mixer can not work without
threads, which is a problem. Yet another reason to write an OpenAL
backend - emscripten supports that natively.

* emscripten: customize the html shell

* emscripten: force "show log" checkbox unchecked initially

* emscripten: remove quit shortcut from main menu (since there's no quit)

* emscripten: log area fixes

* emscripten/webgl: workaround for fullscreen viewport issue

* emscripten: implement frameskip

* emscripter: improve framerate limiter

* align List to at least 8 bytes (shut up warnings)

* fix non-emscripten builds

* improve fullscreen handling, mainly for emscripten

* Workaround to make audio work in chromium

emscripten-core/emscripten#6511

* emscripten: better vsync handling; enable vsync & disable fxaa by default
This commit is contained in:
Andrei Alexeyev 2019-03-09 21:32:32 +02:00 committed by GitHub
parent c8e281bc02
commit 180f9e3856
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
94 changed files with 2505 additions and 830 deletions

View file

@ -34,7 +34,7 @@ Build-only dependencies
^^^^^^^^^^^^^^^^^^^^^^^
- Python >= 3.5
- meson >= 0.45.0 (build system; >=0.48.0 recommended)
- meson >= 0.46.0 (build system; >=0.48.0 recommended)
Optional:

BIN
emscripten/background.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

BIN
emscripten/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

14
emscripten/meson.build Normal file
View file

@ -0,0 +1,14 @@
em_preamble = files('preamble.js')
em_shell = files('shell.html')
if host_machine.system() == 'emscripten'
install_data(
files(
'background.webp',
'scythe.webp',
'favicon.ico',
),
install_dir : bindir
)
endif

20
emscripten/preamble.js Normal file
View file

@ -0,0 +1,20 @@
Module['preRun'].push(function() {
ENV["TAISEI_NOASYNC"] = "1";
ENV["TAISEI_NOUNLOAD"] = "1";
ENV["TAISEI_PREFER_SDL_VIDEODRIVERS"] = "emscripten";
ENV["TAISEI_RENDERER"] = "gles30";
FS.mkdir('/persistent');
FS.mount(IDBFS, {}, '/persistent');
});
function SyncFS(is_load, ccptr) {
FS.syncfs(is_load, function(err) {
Module['ccall'](
'vfs_sync_callback',
null, ["boolean", "string", "number"],
[is_load, err, ccptr]
);
});
};

BIN
emscripten/scythe.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

269
emscripten/shell.html Normal file
View file

@ -0,0 +1,269 @@
<!doctype html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<link rel="icon" type="image/x-icon" href="favicon.ico"/>
<title>Taisei Project — Web version (Experimental!)</title>
<style>
body {
background-image: url('background.webp');
background-color: #111111;
background-repeat: no-repeat;
background-position: 50% 50%;
background-size: cover;
color: #eeeeee;
height: 100%;
margin: 0px;
padding: 0px;
position: absolute;
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
}
html {
background-color: #000000;
height: 100%;
margin: 0px;
padding: 0px;
}
.emscripten {
padding-right: 0;
margin-left: auto;
margin-right: auto;
display: block;
}
textarea.emscripten {
font-family: monospace;
width: 100%;
resize: none;
background: none;
border: 0px none;
color: #eeeeee;
overflow: overlay;
margin: 0px;
}
div.emscripten {
text-align: center;
}
div.emscripten_border {
border: 0px none;
}
div.centered {
position: absolute;
bottom: 50%;
left: 0;
right: 0;
transform: translateY(50%);
background-color: #000000C0;
padding: 16px 0px;
}
/* the canvas *must not* have any border or padding, or mouse coords will be wrong */
canvas.emscripten {
border: 0px none;
background-color: none;
}
#spinner {
overflow: visible;
padding-bottom: 16px;
}
#progress {
margin: 4px;
}
#logContainer {
width: 80%;
margin: auto;
}
#logToggleContainer {
display: /* inline-block */ none;
position: relative;
left: 50%;
transform: translateX(-50%);
opacity: 0.5;
}
progress {
-webkit-appearance: none;
-moz-appearance: none;
border: 0px none;
background-color: #eee;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25) inset;
}
progress::-webkit-progress-bar {
background-color: #eee;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25) inset;
}
progress::-webkit-progress-value {
background-image:
-webkit-linear-gradient(top, rgba(0, 0, 0, 0.25), rgba(255, 255, 255, 0.25)),
-webkit-linear-gradient(left, #5c7da8, #6db0ed);
border-radius: 5px;
background-size: 35px 20px, 100% 100%, 100% 100%;
}
progress::-moz-progress-bar {
background-image:
-moz-linear-gradient(top, rgba(0, 0, 0, 0.25), rgba(255, 255, 255, 0.25)),
-moz-linear-gradient(left, #5c7da8, #6db0ed);
border-radius: 5px;
background-size: 35px 20px, 100% 100%, 100% 100%;
}
div.spinner {
height: 75px;
width: 75px;
margin: 0px auto;
}
img.spinner {
height: 75px;
width: 75px;
margin: 4px auto;
-webkit-animation: rotation 0.8s linear infinite;
-moz-animation: rotation 0.8s linear infinite;
-o-animation: rotation 0.8s linear infinite;
animation: rotation 0.6s linear infinite;
border: 0px none;
}
@-webkit-keyframes rotation {
from { -webkit-transform: rotate(0deg); }
to { -webkit-transform: rotate(360deg); }
}
@-moz-keyframes rotation {
from { -moz-transform: rotate(0deg); }
to { -moz-transform: rotate(360deg); }
}
@-o-keyframes rotation {
from { -o-transform: rotate(0deg); }
to { -o-transform: rotate(360deg); }
}
@keyframes rotation {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="centered">
<div class="spinner" id="spinner"><img src="scythe.webp" class="spinner"/></div>
<div class="emscripten" id="status">Girls are now downloading, please wait warmly…</div>
<div class="emscripten">
<progress value="0" max="100" id="progress" ></progress>
</div>
<div class="emscripten_border" id="canvasContainer" hidden>
<canvas class="emscripten" id="canvas" oncontextmenu="event.preventDefault()" tabindex=-1></canvas>
</div>
<div id="logToggleContainer" hidden>
<input type="checkbox" name="logToggle" id="logToggle" onclick="toggleLog()"/>
<label for="logToggle"> Show log</label>
</div>
<div id="logContainer" hidden>
<textarea class="emscripten" id="output" rows="12" readonly></textarea>
</div>
</div>
<script>
var statusElement = document.getElementById('status');
var progressElement = document.getElementById('progress');
var spinnerElement = document.getElementById('spinner');
var canvasContainerElement = document.getElementById('canvasContainer');
var logToggleElement = document.getElementById('logToggle');
var logContainerElement = document.getElementById('logContainer');
var logOutputElement = document.getElementById('output');
var dlMessage = statusElement.innerText;
logToggleElement.checked = false;
function toggleLog() {
logContainerElement.hidden = !logToggleElement.checked;
logOutputElement.scrollTop = logOutputElement.scrollHeight;
}
var Taisei = {
preRun: [],
postRun: [],
onFirstFrame: function() {
canvasContainerElement.hidden = false;
logToggleContainer.style.display = "inline-block";
Taisei.setStatus('', true);
},
print: (function() {
logOutputElement.value = ''; // clear browser cache
return function(text) {
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
console.log(text);
logOutputElement.value += text + "\n";
logOutputElement.scrollTop = logOutputElement.scrollHeight; // focus on bottom
};
})(),
printErr: function(text) {
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
console.error(text);
},
canvas: (function() {
var canvas = document.getElementById('canvas');
// As a default initial behavior, pop up an alert when webgl context is lost. To make your
// application robust, you may want to override this behavior before shipping!
// See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2
canvas.addEventListener("webglcontextlost", function(e) { alert('WebGL context lost. You will need to reload the page.'); e.preventDefault(); }, false);
return canvas;
})(),
setStatus: function(text, force) {
if (!text && !force) return;
if (!Taisei.setStatus.last) Taisei.setStatus.last = { time: Date.now(), text: '' };
if (text === Taisei.setStatus.last.text) return;
var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
var now = Date.now();
if (m && now - Taisei.setStatus.last.time < 30) return; // if this is a progress update, skip it if too soon
Taisei.setStatus.last.time = now;
Taisei.setStatus.last.text = text;
if (m) {
text = m[1];
progressElement.value = parseInt(m[2])*100;
progressElement.max = parseInt(m[4])*100;
progressElement.hidden = false;
spinnerElement.hidden = false;
} else {
progressElement.value = null;
progressElement.max = null;
progressElement.hidden = true;
if (!text) spinnerElement.hidden = true;
}
statusElement.innerText = text.replace(/^Downloading(?: data)?\.\.\./, dlMessage).replace('...', '…');
console.log("[STATUS] " + statusElement.innerText);
},
totalDependencies: 0,
monitorRunDependencies: function(left) {
this.totalDependencies = Math.max(this.totalDependencies, left);
Taisei.setStatus(left ? 'Preparing… (' + (this.totalDependencies-left) + '/' + this.totalDependencies + ')' : 'All downloads complete.');
}
};
window.onerror = function(error) {
Taisei.setStatus('Error: ' + error);
};
</script>
{{{ SCRIPT }}}
</body>
</html>

14
external/meson.build vendored
View file

@ -1,5 +1,11 @@
install_data(
join_paths('gamecontrollerdb', 'gamecontrollerdb.txt'),
install_dir : data_path
)
gamecontrollerdb_name = 'gamecontrollerdb.txt'
gamecontrollerdb_relpath = join_paths('gamecontrollerdb', gamecontrollerdb_name)
gamecontrollerdb_path = join_paths(meson.current_source_dir(), gamecontrollerdb_relpath)
if host_machine.system() != 'emscripten'
install_data(
gamecontrollerdb_relpath,
install_dir : data_path
)
endif

View file

@ -1,7 +1,7 @@
project('taisei', 'c',
license : 'MIT',
version : 'v1.3-dev',
meson_version : '>=0.45.0',
meson_version : '>=0.46.0',
default_options : [
'c_std=c11',
@ -34,6 +34,7 @@ taisei_c_args = [
'-Wabsolute-value',
'-Wcast-align',
'-Wcast-align=strict',
'-Wcast-function-type',
'-Wclobbered',
'-Wduplicated-branches',
'-Wduplicated-cond',
@ -90,7 +91,7 @@ if sm_check.stderr() != ''
warning('Submodule check completed with errors:\n@0@'.format(sm_check.stderr()))
endif
static = get_option('static')
static = get_option('static') or (host_machine.system() == 'emscripten')
dep_freetype = dependency('freetype2', required : true, static : static)
dep_png = dependency('libpng', version : '>=1.5', required : true, static : static)
@ -130,9 +131,14 @@ if host_machine.system() == 'windows'
taisei_deps += cc.find_library('shlwapi')
endif
package_data = get_option('package_data')
enable_zip = get_option('enable_zip')
package_data = (package_data == 'auto' ? enable_zip : package_data == 'true')
if host_machine.system() == 'emscripten'
package_data = false
enable_zip = false
else
package_data = get_option('package_data')
enable_zip = get_option('enable_zip')
package_data = (package_data == 'auto' ? enable_zip : package_data == 'true')
endif
if enable_zip
if not dep_zip.found()
@ -163,6 +169,9 @@ config.set('TAISEI_BUILDCONF_HAVE_MAX_ALIGN_T', cc.compiles('#include <stddef.h>
prefer_relpath_systems = [
'windows',
]
force_relpath_systems = [
'emscripten',
]
@ -183,7 +192,9 @@ if macos_app_bundle
else
datadir = get_option('datadir')
if get_option('install_relative') == 'auto'
if force_relpath_systems.contains(host_machine.system())
config.set('TAISEI_BUILDCONF_RELATIVE_DATA_PATH', true)
elif get_option('install_relative') == 'auto'
config.set('TAISEI_BUILDCONF_RELATIVE_DATA_PATH', prefer_relpath_systems.contains(host_machine.system()))
else
config.set('TAISEI_BUILDCONF_RELATIVE_DATA_PATH', get_option('install_relative') == 'true')
@ -275,6 +286,7 @@ endif
version_deps = []
subdir('misc')
subdir('emscripten')
subdir('external')
subdir('resources')
subdir('doc')

View file

@ -37,7 +37,7 @@ if validate_glsl
fname = '@0@'.format(src)
stage = fname.split('.')[-2]
glsl_targets += custom_target(fname.underscorify(),
spirv = custom_target('SPIRV_' + fname.underscorify(),
input : src,
output : '@BASENAME@.spv',
command : [
@ -48,6 +48,26 @@ if validate_glsl
build_by_default : true,
depfile : '@0@.d'.format(fname.underscorify()),
)
spirv_targets += spirv
if transpile_glsl
essl = custom_target('ESSL_' + fname.underscorify(),
input : spirv,
output : '@BASENAME@.glsl',
command : [
spvc_command,
'--output', '@OUTPUT@', '@INPUT@',
spvc_args,
get_variable('spvc_@0@_args'.format(stage)),
],
install : false,
build_by_default : true,
depfile : '@0@.d'.format(fname.underscorify()),
)
essl_targets += essl
endif
endforeach
endif
# @end validate

View file

@ -91,9 +91,8 @@ subdirs = [
# @end subdirs
]
glsl_targets = []
validate_glsl = false
spirv_targets = []
essl_targets = []
glslc_args = [
# '-fauto-bind-uniforms',
@ -124,9 +123,31 @@ glslc_vert_args = [
'-DVERT_STAGE',
]
if get_option('validate_glsl') != 'false'
spvc_args = [
'--version', '300',
'--es',
'--remove-unused-variables',
]
spvc_frag_args = [
'--stage', 'frag',
]
spvc_vert_args = [
'--stage', 'vert',
]
if host_machine.system() == 'emscripten'
validate_glsl = 'true'
transpile_glsl = true
else
validate_glsl = get_option('validate_glsl')
transpile_glsl = false
endif
if validate_glsl != 'false'
glslc_command = find_program('glslc',
required : (get_option('validate_glsl') == 'true')
required : (validate_glsl == 'true')
)
if glslc_command.found()
@ -143,13 +164,23 @@ if get_option('validate_glsl') != 'false'
else
warning(test_result.stderr())
if get_option('validate_glsl') == 'auto'
if validate_glsl == 'auto'
warning('glslc test failed, you probably have an incompatible version. GLSL validation will be disabled.')
validate_glsl = false
else
error('glslc test failed, you probably have an incompatible version.')
endif
endif
else
validate_glsl = false
endif
if validate_glsl and transpile_glsl
spvc_command = find_program('spirv-cross', required : true)
glslc_args += ['-Os', '-g']
endif
else
validate_glsl = false
endif
# @begin validate
@ -158,7 +189,7 @@ if validate_glsl
fname = '@0@'.format(src)
stage = fname.split('.')[-2]
glsl_targets += custom_target(fname.underscorify(),
spirv = custom_target('SPIRV_' + fname.underscorify(),
input : src,
output : '@BASENAME@.spv',
command : [
@ -169,6 +200,26 @@ if validate_glsl
build_by_default : true,
depfile : '@0@.d'.format(fname.underscorify()),
)
spirv_targets += spirv
if transpile_glsl
essl = custom_target('ESSL_' + fname.underscorify(),
input : spirv,
output : '@BASENAME@.glsl',
command : [
spvc_command,
'--output', '@OUTPUT@', '@INPUT@',
spvc_args,
get_variable('spvc_@0@_args'.format(stage)),
],
install : false,
build_by_default : true,
depfile : '@0@.d'.format(fname.underscorify()),
)
essl_targets += essl
endif
endforeach
endif
# @end validate
@ -176,3 +227,5 @@ endif
foreach sd : subdirs
subdir(sd)
endforeach
shaders_build_dir = meson.current_build_dir()

View file

@ -5,6 +5,50 @@ packages = [
'00-taisei',
]
if host_machine.system() == 'emscripten'
em_data_prefix = '/@0@'.format(config.get_unquoted('TAISEI_BUILDCONF_DATA_PATH'))
em_bundles = ['gfx', 'misc']
if get_option('a_default') != 'null'
em_bundles += ['bgm', 'sfx']
endif
em_bundle_gfx_patterns = [
'gfx/*.png',
'gfx/*.webp',
'fonts/*.ttf',
'fonts/*.otf',
]
em_bundle_misc_patterns = [
'gfx/*.ani',
'gfx/*.spr',
'gfx/*.tex',
'fonts/*.font',
'models/*.obj',
# We don't want to include the shader sources here, we're going to translate them first.
'shader/*.prog'
]
em_bundle_bgm_patterns = [
'bgm/*',
]
em_bundle_sfx_patterns = [
'sfx/*',
]
foreach bundle : em_bundles
set_variable('em_bundle_@0@_deps'.format(bundle), [])
set_variable('em_bundle_@0@_files'.format(bundle), [])
set_variable('em_bundle_@0@_packer_args'.format(bundle), [])
endforeach
# These are all text files that compress well
em_bundle_misc_packer_args += ['--lz4']
endif
foreach pkg : packages
pkg_pkgdir = '@0@.pkgdir'.format(pkg)
pkg_zip = '@0@.zip'.format(pkg)
@ -12,7 +56,26 @@ foreach pkg : packages
subdir(pkg_pkgdir)
if package_data
if host_machine.system() == 'emscripten'
foreach bundle : em_bundles
var_patterns = 'em_bundle_@0@_patterns'.format(bundle)
var_files = 'em_bundle_@0@_files'.format(bundle)
var_packer_args = 'em_bundle_@0@_packer_args'.format(bundle)
glob_result = run_command(glob_command, pkg_path, get_variable(var_patterns))
assert(glob_result.returncode() == 0, 'Glob script failed')
foreach file : glob_result.stdout().strip().split('\n')
fpath = join_paths(meson.current_source_dir(), pkg_pkgdir, file)
set_variable(var_files, get_variable(var_files) + [fpath])
set_variable(var_packer_args, get_variable(var_packer_args) + [
'--preload', '@0@@@1@/@2@'.format(fpath, em_data_prefix, file)
])
endforeach
endforeach
elif package_data
custom_target(pkg_zip,
command : [pack_command,
pkg_path,
@ -30,3 +93,69 @@ foreach pkg : packages
install_subdir(pkg_pkgdir, install_dir : data_path, exclude_files : glob_result.stdout().split('\n'))
endif
endforeach
if host_machine.system() == 'emscripten'
# First add some stuff that isn't sourced from resources/
em_bundle_misc_files += gamecontrollerdb_path
em_bundle_misc_packer_args += [
'--preload', '@0@@@1@/@2@'.format(gamecontrollerdb_path, em_data_prefix, gamecontrollerdb_name)
]
foreach shader : essl_targets
# This one is especially dirty!
abs_path = shader.full_path()
assert(abs_path.startswith(shaders_build_dir), 'Assumption about shader output location violated')
rel_path = '@0@/shader@1@'.format(em_data_prefix, abs_path.split(shaders_build_dir)[1])
em_bundle_misc_deps += shader
em_bundle_misc_packer_args += [
'--preload', '@0@@@1@'.format(abs_path, rel_path)
]
endforeach
packer = find_program('file_packager') # should be provided by cross file
em_bundle_link_args = [] # We'll pass these to the final emcc linking step
# Finally, set up build targets for our bundles
foreach bundle : em_bundles
var_files = 'em_bundle_@0@_files'.format(bundle)
var_deps = 'em_bundle_@0@_deps'.format(bundle)
var_packer_args = 'em_bundle_@0@_packer_args'.format(bundle)
data_name = 'bundle_@0@.data'.format(bundle)
loader_name = 'bundle_@0@.js'.format(bundle)
out_loader = join_paths(meson.current_build_dir(), '@0@.raw'.format(loader_name))
bundle_data = custom_target(data_name,
command : [
packer,
'@OUTPUT0@',
'--js-output=@0@'.format(out_loader), # No, this one does not accept "--js-output foobar.js"
'--use-preload-cache',
'--no-heap-copy',
'--from-emcc',
get_variable(var_packer_args)
],
output : data_name,
depends : get_variable(var_deps),
depend_files : get_variable(var_files),
install : true,
install_dir : bindir,
)
bundle_loader = custom_target(loader_name,
command : [
em_set_bundle_uuid_command,
out_loader,
'--sha1', bundle_data,
'--output', '@OUTPUT@',
],
output : loader_name,
install : false,
)
em_bundle_link_args += ['--pre-js', bundle_loader]
endforeach
endif

66
scripts/em-set-bundle-uuid.py Executable file
View file

@ -0,0 +1,66 @@
#!/usr/bin/env python3
from taiseilib.common import (
run_main,
update_text_file,
)
import argparse
import hashlib
import json
import re
from pathlib import Path
meta_re = re.compile(r'(.*loadPackage\()({.*?})(\);.*)', re.DOTALL)
def main(args):
parser = argparse.ArgumentParser(description='Change package UUID in JavaScript loader generated by Emscripten\'s file_packager.py', prog=args[0])
parser.add_argument('loader',
help='the .js loader file',
metavar='FILE',
type=Path,
)
parser.add_argument('--output', '-o',
help='write result to FILE (default: overwrite input)',
metavar='FILE',
type=Path,
)
g = parser.add_mutually_exclusive_group(required=True)
g.add_argument('--uuid',
help='manually specify an UUID',
metavar='UUID',
type=str,
)
g.add_argument('--sha1',
help='take SHA1 of FILE and use that as an UUID',
metavar='FILE',
type=Path,
)
args = parser.parse_args(args[1:])
if args.uuid is None:
args.uuid = hashlib.sha1(args.sha1.read_bytes()).hexdigest()
if args.output is None:
args.output = args.loader
pre, meta, post = meta_re.match(args.loader.read_text()).groups()
meta = json.loads(meta)
meta['package_uuid'] = args.uuid
meta = json.dumps(meta, separators=(',', ':'), check_circular=False, ensure_ascii=False)
update_text_file(args.output, pre + meta + post)
if __name__ == '__main__':
run_main(main)

View file

@ -52,5 +52,6 @@ def main(args):
print("Generated package {}".format(str(args.output)))
if __name__ == '__main__':
run_main(main)

View file

@ -6,6 +6,7 @@ from taiseilib.common import (
)
from pathlib import Path
import fnmatch
def main(args):
@ -25,9 +26,12 @@ def main(args):
args = parser.parse_args(args[1:])
for pattern in args.patterns:
for path in args.directory.glob(pattern):
print(path.relative_to(args.directory))
for path in (p.relative_to(args.directory) for p in args.directory.rglob('*')):
path = str(path)
for pattern in args.patterns:
if fnmatch.fnmatchcase(path, pattern):
print(path)
break
if __name__ == '__main__':

View file

@ -125,3 +125,6 @@ glob_command = [python_thunk, glob_script]
check_submodules_script = files('check-submodules.py')
check_submodules_command = [python_thunk, check_submodules_script]
em_set_bundle_uuid_script = files('em-set-bundle-uuid.py')
em_set_bundle_uuid_command = [python_thunk, em_set_bundle_uuid_script]

View file

@ -42,7 +42,7 @@ validation_code = '''if validate_glsl
fname = '@0@'.format(src)
stage = fname.split('.')[-2]
glsl_targets += custom_target(fname.underscorify(),
spirv = custom_target('SPIRV_' + fname.underscorify(),
input : src,
output : '@BASENAME@.spv',
command : [
@ -53,6 +53,26 @@ validation_code = '''if validate_glsl
build_by_default : true,
depfile : '@0@.d'.format(fname.underscorify()),
)
spirv_targets += spirv
if transpile_glsl
essl = custom_target('ESSL_' + fname.underscorify(),
input : spirv,
output : '@BASENAME@.glsl',
command : [
spvc_command,
'--output', '@OUTPUT@', '@INPUT@',
spvc_args,
get_variable('spvc_@0@_args'.format(stage)),
],
install : false,
build_by_default : true,
depfile : '@0@.d'.format(fname.underscorify()),
)
essl_targets += essl
endif
endforeach
endif'''

View file

@ -200,4 +200,5 @@ int cli_args(int argc, char **argv, CLIAction *a) {
void free_cli_action(CLIAction *a) {
free(a->filename);
a->filename = NULL;
}

View file

@ -390,14 +390,24 @@ static bool config_set(const char *key, const char *val, void *data) {
typedef void (*ConfigUpgradeFunc)(void);
static void config_upgrade_1(void) {
// disable vsync by default
config_set_int(CONFIG_VSYNC, 0);
// reset vsync to the default value
config_set_int(CONFIG_VSYNC, CONFIG_VSYNC_DEFAULT);
// this version also changes meaning of the vsync value
// previously it was: 0 = on, 1 = off, 2 = adaptive, because lachs0r doesn't know how my absolutely genius options menu works.
// now it is: 0 = off, 1 = on, 2 = adaptive, as it should be.
}
#ifdef __EMSCRIPTEN__
static void config_upgrade_2(void) {
// emscripten defaults for these have been changed
config_set_int(CONFIG_VSYNC, CONFIG_VSYNC_DEFAULT);
config_set_int(CONFIG_FXAA, CONFIG_FXAA_DEFAULT);
}
#else
#define config_upgrade_2 NULL
#endif
static ConfigUpgradeFunc config_upgrades[] = {
/*
To bump the config version and add an upgrade state, simply append an upgrade function to this array.
@ -408,6 +418,7 @@ static ConfigUpgradeFunc config_upgrades[] = {
*/
config_upgrade_1,
config_upgrade_2,
};
static void config_apply_upgrades(int start) {

View file

@ -74,6 +74,15 @@
CONFIGDEF_GPKEYBINDING(KEY_SKIP, "gamepad_key_skip", GAMEPAD_BUTTON_B) \
#ifdef __EMSCRIPTEN__
#define CONFIG_VSYNC_DEFAULT 1
#define CONFIG_FXAA_DEFAULT 0
#else
#define CONFIG_VSYNC_DEFAULT 0
#define CONFIG_FXAA_DEFAULT 1
#endif
#define CONFIGDEFS \
/* @version must be on top. don't change its default value here, it does nothing. */ \
CONFIGDEF_INT (VERSION, "@version", 0) \
@ -85,7 +94,7 @@
CONFIGDEF_INT (VID_HEIGHT, "vid_height", RESY) \
CONFIGDEF_INT (VID_RESIZABLE, "vid_resizable", 0) \
CONFIGDEF_INT (VID_FRAMESKIP, "vid_frameskip", 1) \
CONFIGDEF_INT (VSYNC, "vsync", 0) \
CONFIGDEF_INT (VSYNC, "vsync", CONFIG_VSYNC_DEFAULT) \
CONFIGDEF_INT (MIXER_CHUNKSIZE, "mixer_chunksize", 1024) \
CONFIGDEF_FLOAT (SFX_VOLUME, "sfx_volume", 0.5) \
CONFIGDEF_FLOAT (BGM_VOLUME, "bgm_volume", 1.0) \
@ -100,7 +109,7 @@
CONFIGDEF_INT (SHOT_INVERTED, "shot_inverted", 0) \
CONFIGDEF_INT (FOCUS_LOSS_PAUSE, "focus_loss_pause", 1) \
CONFIGDEF_INT (PARTICLES, "particles", 1) \
CONFIGDEF_INT (FXAA, "fxaa", 1) \
CONFIGDEF_INT (FXAA, "fxaa", CONFIG_FXAA_DEFAULT) \
CONFIGDEF_INT (POSTPROCESS, "postprocess", 2) \
CONFIGDEF_INT (HEALTHBAR_STYLE, "healthbar_style", 1) \
CONFIGDEF_INT (SKIP_SPEED, "skip_speed", 10) \

View file

@ -27,6 +27,7 @@ static struct {
float panelalpha;
int end;
bool skipable;
CallChain cc;
} credits;
#define CREDITS_ENTRY_FADEIN 200.0
@ -417,7 +418,7 @@ void credits_preload(void) {
NULL);
}
static FrameAction credits_logic_frame(void *arg) {
static LogicFrameAction credits_logic_frame(void *arg) {
update_transition();
events_poll(NULL, 0);
credits_process();
@ -432,14 +433,19 @@ static FrameAction credits_logic_frame(void *arg) {
}
}
static FrameAction credits_render_frame(void *arg) {
static RenderFrameAction credits_render_frame(void *arg) {
credits_draw();
return RFRAME_SWAP;
}
void credits_loop(void) {
static void credits_end_loop(void *ctx) {
credits_free();
run_call_chain(&credits.cc, NULL);
}
void credits_enter(CallChain next) {
credits_preload();
credits_init();
loop_at_fps(credits_logic_frame, credits_render_frame, NULL, FPS);
credits_free();
credits.cc = next;
eventloop_enter(&credits, credits_logic_frame, credits_render_frame, credits_end_loop, FPS);
}

View file

@ -11,7 +11,9 @@
#include "taisei.h"
void credits_loop(void);
#include "eventloop/eventloop.h"
void credits_enter(CallChain next);
void credits_preload(void);
#endif // IGUARD_credits_h

View file

@ -13,18 +13,19 @@
#include "video.h"
#include "progress.h"
struct EndingEntry {
typedef struct EndingEntry {
char *msg;
Sprite *sprite;
int time;
};
} EndingEntry;
struct Ending {
typedef struct Ending {
EndingEntry *entries;
int count;
int duration;
int pos;
};
CallChain cc;
} Ending;
static void track_ending(int ending) {
assert(ending >= 0 && ending < NUM_ENDINGS);
@ -150,9 +151,7 @@ void good_ending_reimu(Ending *e) {
track_ending(ENDING_GOOD_3);
}
static void create_ending(Ending *e) {
memset(e, 0, sizeof(Ending));
static void init_ending(Ending *e) {
if(global.plr.continues_used) {
global.plr.mode->character->ending.bad(e);
} else {
@ -234,7 +233,7 @@ static bool ending_input_handler(SDL_Event *event, void *arg) {
return false;
}
static FrameAction ending_logic_frame(void *arg) {
static LogicFrameAction ending_logic_frame(void *arg) {
Ending *e = arg;
update_transition();
@ -267,7 +266,7 @@ static FrameAction ending_logic_frame(void *arg) {
return LFRAME_WAIT;
}
static FrameAction ending_render_frame(void *arg) {
static RenderFrameAction ending_render_frame(void *arg) {
Ending *e = arg;
if(e->pos < e->count-1)
@ -277,13 +276,24 @@ static FrameAction ending_render_frame(void *arg) {
return RFRAME_SWAP;
}
void ending_loop(void) {
Ending e;
static void ending_loop_end(void *ctx) {
Ending *e = ctx;
CallChain cc = e->cc;
free_ending(e);
free(e);
run_call_chain(&cc, NULL);
}
void ending_enter(CallChain next) {
ending_preload();
create_ending(&e);
Ending *e = calloc(1, sizeof(Ending));
init_ending(e);
e->cc = next;
global.frames = 0;
set_ortho(SCREEN_W, SCREEN_H);
start_bgm("ending");
loop_at_fps(ending_logic_frame, ending_render_frame, &e, FPS);
free_ending(&e);
eventloop_enter(e, ending_logic_frame, ending_render_frame, ending_loop_end, FPS);
}

View file

@ -11,7 +11,7 @@
#include "taisei.h"
#include "resource/texture.h"
#include "eventloop/eventloop.h"
enum {
ENDING_FADE_OUT = 200,
@ -56,10 +56,9 @@ static inline attr_must_inline bool ending_is_bad(EndingID end) {
#undef ENDING
}
typedef struct EndingEntry EndingEntry;
typedef struct Ending Ending;
void ending_loop(void);
void ending_enter(CallChain next);
void ending_preload(void);
/*

114
src/eventloop/eventloop.c Normal file
View file

@ -0,0 +1,114 @@
/*
* This software is licensed under the terms of the MIT-License
* See COPYING for further information.
* ---
* Copyright (c) 2011-2019, Lukas Weber <laochailan@web.de>.
* Copyright (c) 2012-2019, Andrei Alexeyev <akari@alienslab.net>.
*/
#include "taisei.h"
#include "eventloop_private.h"
#include "util.h"
#include "global.h"
#include "video.h"
struct evloop_s evloop;
void eventloop_enter(void *context, LogicFrameFunc frame_logic, RenderFrameFunc frame_render, PostLoopFunc on_leave, uint target_fps) {
assert(is_main_thread());
assume(evloop.stack_ptr < evloop.stack + EVLOOP_STACK_SIZE - 1);
LoopFrame *frame;
if(evloop.stack_ptr == NULL) {
frame = evloop.stack_ptr = evloop.stack;
} else {
frame = ++evloop.stack_ptr;
}
frame->context = context;
frame->logic = frame_logic;
frame->render = frame_render;
frame->on_leave = on_leave;
frame->frametime = HRTIME_RESOLUTION / target_fps;
frame->prev_logic_action = LFRAME_WAIT;
}
void eventloop_leave(void) {
assert(is_main_thread());
assume(evloop.stack_ptr != NULL);
LoopFrame *frame = evloop.stack_ptr;
if(evloop.stack_ptr == evloop.stack) {
evloop.stack_ptr = NULL;
} else {
--evloop.stack_ptr;
}
if(frame->on_leave != NULL) {
frame->on_leave(frame->context);
}
}
LogicFrameAction run_logic_frame(LoopFrame *frame) {
assert(frame == evloop.stack_ptr);
if(frame->prev_logic_action == LFRAME_STOP) {
return LFRAME_STOP;
}
LogicFrameAction a = frame->logic(frame->context);
if(taisei_quit_requested()) {
a = LFRAME_STOP;
}
frame->prev_logic_action = a;
return a;
}
LogicFrameAction handle_logic(LoopFrame **pframe, const FrameTimes *ftimes) {
LogicFrameAction lframe_action;
uint cnt = 0;
do {
lframe_action = run_logic_frame(*pframe);
while(evloop.stack_ptr != *pframe) {
*pframe = evloop.stack_ptr;
lframe_action = run_logic_frame(*pframe);
cnt = UINT_MAX; // break out of the outer loop
}
} while(
lframe_action == LFRAME_SKIP &&
++cnt < config_get_int(CONFIG_SKIP_SPEED)
);
if(lframe_action == LFRAME_STOP) {
eventloop_leave();
*pframe = evloop.stack_ptr;
if(*pframe == NULL) {
return LFRAME_STOP;
}
}
fpscounter_update(&global.fps.logic);
return lframe_action;
}
RenderFrameAction run_render_frame(LoopFrame *frame) {
attr_unused LoopFrame *stack_prev = evloop.stack_ptr;
r_framebuffer_clear(NULL, CLEAR_ALL, RGBA(0, 0, 0, 1), 1);
RenderFrameAction a = frame->render(frame->context);
assert(evloop.stack_ptr == stack_prev);
if(a == RFRAME_SWAP) {
video_swap_buffers();
}
fpscounter_update(&global.fps.render);
return a;
}

93
src/eventloop/eventloop.h Normal file
View file

@ -0,0 +1,93 @@
/*
* This software is licensed under the terms of the MIT-License
* See COPYING for further information.
* ---
* Copyright (c) 2011-2019, Lukas Weber <laochailan@web.de>.
* Copyright (c) 2012-2019, Andrei Alexeyev <akari@alienslab.net>.
*/
#ifndef IGUARD_eventloop_eventloop_h
#define IGUARD_eventloop_eventloop_h
#include "taisei.h"
typedef enum RenderFrameAction {
RFRAME_SWAP,
RFRAME_DROP,
} RenderFrameAction;
typedef enum LogicFrameAction {
LFRAME_WAIT,
LFRAME_SKIP,
LFRAME_STOP,
} LogicFrameAction;
typedef LogicFrameAction (*LogicFrameFunc)(void *context);
typedef RenderFrameAction (*RenderFrameFunc)(void *context);
typedef void (*PostLoopFunc)(void *context);
#ifdef DEBUG_CALLCHAIN
#include "util/debug.h"
#include "log.h"
#endif
typedef struct CallChainResult {
void *ctx;
void *result;
} CallChainResult;
typedef struct CallChain {
void (*callback)(CallChainResult result);
void *ctx;
#ifdef DEBUG_CALLCHAIN
DebugInfo _debug_;
#endif
} CallChain;
#ifdef DEBUG_CALLCHAIN
#define CALLCHAIN(callback, ctx) ((CallChain) { (callback), (ctx), _DEBUG_INFO_INITIALIZER_ })
#else
#define CALLCHAIN(callback, ctx) ((CallChain) { (callback), (ctx) })
#endif
#define NO_CALLCHAIN CALLCHAIN(NULL, NULL)
#define CALLCHAIN_RESULT(ctx, result) ((CallChainResult) { (ctx), (result) })
void eventloop_enter(
void *context,
LogicFrameFunc frame_logic,
RenderFrameFunc frame_render,
PostLoopFunc on_leave,
uint target_fps
) attr_nonnull(1, 2, 3);
void eventloop_run(void);
#ifdef DEBUG_CALLCHAIN
attr_must_inline
static inline void run_call_chain(CallChain *cc, void *result, DebugInfo caller_dbg) {
if(cc->callback != NULL) {
log_debug("Calling CC set in %s (%s:%u)",
cc->_debug_.func, cc->_debug_.file, cc->_debug_.line);
log_debug(" from %s (%s:%u)",
caller_dbg.func, caller_dbg.file, caller_dbg.line);
cc->callback(CALLCHAIN_RESULT(cc->ctx, result));
} else {
log_debug("Dead end at %s (%s:%u)",
caller_dbg.func, caller_dbg.file, caller_dbg.line
);
}
}
#define run_call_chain(cc, result) run_call_chain(cc, result, _DEBUG_INFO_)
#else
attr_must_inline
static inline void run_call_chain(CallChain *cc, void *result) {
if(cc->callback != NULL) {
cc->callback(CALLCHAIN_RESULT(cc->ctx, result));
}
}
#endif
#endif // IGUARD_eventloop_eventloop_h

View file

@ -0,0 +1,47 @@
/*
* This software is licensed under the terms of the MIT-License
* See COPYING for further information.
* ---
* Copyright (c) 2011-2019, Lukas Weber <laochailan@web.de>.
* Copyright (c) 2012-2019, Andrei Alexeyev <akari@alienslab.net>.
*/
#ifndef IGUARD_eventloop_eventloop_private_h
#define IGUARD_eventloop_eventloop_private_h
#include "taisei.h"
#include "eventloop.h"
#include "hirestime.h"
typedef struct LoopFrame LoopFrame;
#define EVLOOP_STACK_SIZE 32
struct LoopFrame {
void *context;
LogicFrameFunc logic;
RenderFrameFunc render;
PostLoopFunc on_leave;
hrtime_t frametime;
LogicFrameAction prev_logic_action;