Merge pull request #39604 from Faless/js/3months_backport
[HTML5 - 3.2] Backport most changes/improvement in master.
This commit is contained in:
commit
34c5133e6a
|
@ -312,14 +312,14 @@ WebRTCDataChannelJS::WebRTCDataChannelJS(int js_id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var len = buffer.length*buffer.BYTES_PER_ELEMENT;
|
var len = buffer.length*buffer.BYTES_PER_ELEMENT;
|
||||||
var out = Module._malloc(len);
|
var out = _malloc(len);
|
||||||
Module.HEAPU8.set(buffer, out);
|
HEAPU8.set(buffer, out);
|
||||||
ccall("_emrtc_on_ch_message",
|
ccall("_emrtc_on_ch_message",
|
||||||
"void",
|
"void",
|
||||||
["number", "number", "number", "number"],
|
["number", "number", "number", "number"],
|
||||||
[c_ptr, out, len, is_string]
|
[c_ptr, out, len, is_string]
|
||||||
);
|
);
|
||||||
Module._free(out);
|
_free(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
}, this, js_id);
|
}, this, js_id);
|
||||||
|
|
|
@ -142,14 +142,14 @@ Error EMWSClient::connect_to_host(String p_host, String p_path, uint16_t p_port,
|
||||||
|
|
||||||
}
|
}
|
||||||
var len = buffer.length*buffer.BYTES_PER_ELEMENT;
|
var len = buffer.length*buffer.BYTES_PER_ELEMENT;
|
||||||
var out = Module._malloc(len);
|
var out = _malloc(len);
|
||||||
Module.HEAPU8.set(buffer, out);
|
HEAPU8.set(buffer, out);
|
||||||
ccall("_esws_on_message",
|
ccall("_esws_on_message",
|
||||||
"void",
|
"void",
|
||||||
["number", "number", "number", "number"],
|
["number", "number", "number", "number"],
|
||||||
[c_ptr, out, len, is_string]
|
[c_ptr, out, len, is_string]
|
||||||
);
|
);
|
||||||
Module._free(out);
|
_free(out);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.addEventListener("error", function (event) {
|
socket.addEventListener("error", function (event) {
|
||||||
|
|
|
@ -10,32 +10,56 @@ javascript_files = [
|
||||||
"os_javascript.cpp",
|
"os_javascript.cpp",
|
||||||
]
|
]
|
||||||
|
|
||||||
build = env.add_program(["#bin/godot${PROGSUFFIX}.js", "#bin/godot${PROGSUFFIX}.wasm"], javascript_files)
|
build_targets = ["#bin/godot${PROGSUFFIX}.js", "#bin/godot${PROGSUFFIX}.wasm"]
|
||||||
js, wasm = build
|
if env["threads_enabled"]:
|
||||||
|
build_targets.append("#bin/godot${PROGSUFFIX}.worker.js")
|
||||||
|
|
||||||
|
build = env.add_program(build_targets, javascript_files)
|
||||||
|
|
||||||
js_libraries = [
|
js_libraries = [
|
||||||
"http_request.js",
|
"native/http_request.js",
|
||||||
]
|
]
|
||||||
for lib in js_libraries:
|
for lib in js_libraries:
|
||||||
env.Append(LINKFLAGS=["--js-library", env.File(lib).path])
|
env.Append(LINKFLAGS=["--js-library", env.File(lib).path])
|
||||||
env.Depends(build, js_libraries)
|
env.Depends(build, js_libraries)
|
||||||
|
|
||||||
js_modules = [
|
js_pre = [
|
||||||
"id_handler.js",
|
"native/id_handler.js",
|
||||||
|
"native/utils.js",
|
||||||
]
|
]
|
||||||
for module in js_modules:
|
for js in js_pre:
|
||||||
env.Append(LINKFLAGS=["--pre-js", env.File(module).path])
|
env.Append(LINKFLAGS=["--pre-js", env.File(js).path])
|
||||||
env.Depends(build, js_modules)
|
env.Depends(build, js_pre)
|
||||||
|
|
||||||
wrapper_start = env.File("pre.js")
|
engine = [
|
||||||
wrapper_end = env.File("engine.js")
|
"engine/preloader.js",
|
||||||
js_wrapped = env.Textfile("#bin/godot", [wrapper_start, js, wrapper_end], TEXTFILESUFFIX="${PROGSUFFIX}.wrapped.js")
|
"engine/utils.js",
|
||||||
|
"engine/engine.js",
|
||||||
|
]
|
||||||
|
externs = [env.File("#platform/javascript/engine/externs.js")]
|
||||||
|
js_engine = env.CreateEngineFile("#bin/godot${PROGSUFFIX}.engine.js", engine, externs)
|
||||||
|
env.Depends(js_engine, externs)
|
||||||
|
|
||||||
|
wrap_list = [
|
||||||
|
build[0],
|
||||||
|
js_engine,
|
||||||
|
]
|
||||||
|
js_wrapped = env.Textfile("#bin/godot", [env.File(f) for f in wrap_list], TEXTFILESUFFIX="${PROGSUFFIX}.wrapped.js")
|
||||||
|
|
||||||
zip_dir = env.Dir("#bin/.javascript_zip")
|
zip_dir = env.Dir("#bin/.javascript_zip")
|
||||||
zip_files = env.InstallAs(
|
binary_name = "godot.tools" if env["tools"] else "godot"
|
||||||
[zip_dir.File("godot.js"), zip_dir.File("godot.wasm"), zip_dir.File("godot.html")],
|
out_files = [
|
||||||
[js_wrapped, wasm, "#misc/dist/html/full-size.html"],
|
zip_dir.File(binary_name + ".js"),
|
||||||
)
|
zip_dir.File(binary_name + ".wasm"),
|
||||||
|
zip_dir.File(binary_name + ".html"),
|
||||||
|
]
|
||||||
|
html_file = "#misc/dist/html/full-size.html"
|
||||||
|
in_files = [js_wrapped, build[1], html_file]
|
||||||
|
if env["threads_enabled"]:
|
||||||
|
in_files.append(build[2])
|
||||||
|
out_files.append(zip_dir.File(binary_name + ".worker.js"))
|
||||||
|
|
||||||
|
zip_files = env.InstallAs(out_files, in_files)
|
||||||
env.Zip(
|
env.Zip(
|
||||||
"#bin/godot",
|
"#bin/godot",
|
||||||
zip_files,
|
zip_files,
|
||||||
|
|
|
@ -37,22 +37,18 @@
|
||||||
AudioDriverJavaScript *AudioDriverJavaScript::singleton = NULL;
|
AudioDriverJavaScript *AudioDriverJavaScript::singleton = NULL;
|
||||||
|
|
||||||
const char *AudioDriverJavaScript::get_name() const {
|
const char *AudioDriverJavaScript::get_name() const {
|
||||||
|
|
||||||
return "JavaScript";
|
return "JavaScript";
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_js_mix() {
|
extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_js_mix() {
|
||||||
|
|
||||||
AudioDriverJavaScript::singleton->mix_to_js();
|
AudioDriverJavaScript::singleton->mix_to_js();
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_process_capture(float sample) {
|
extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_process_capture(float sample) {
|
||||||
|
|
||||||
AudioDriverJavaScript::singleton->process_capture(sample);
|
AudioDriverJavaScript::singleton->process_capture(sample);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioDriverJavaScript::mix_to_js() {
|
void AudioDriverJavaScript::mix_to_js() {
|
||||||
|
|
||||||
int channel_count = get_total_channels_by_speaker_mode(get_speaker_mode());
|
int channel_count = get_total_channels_by_speaker_mode(get_speaker_mode());
|
||||||
int sample_count = memarr_len(internal_buffer) / channel_count;
|
int sample_count = memarr_len(internal_buffer) / channel_count;
|
||||||
int32_t *stream_buffer = reinterpret_cast<int32_t *>(internal_buffer);
|
int32_t *stream_buffer = reinterpret_cast<int32_t *>(internal_buffer);
|
||||||
|
@ -63,24 +59,24 @@ void AudioDriverJavaScript::mix_to_js() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioDriverJavaScript::process_capture(float sample) {
|
void AudioDriverJavaScript::process_capture(float sample) {
|
||||||
|
|
||||||
int32_t sample32 = int32_t(sample * 32768.f) * (1U << 16);
|
int32_t sample32 = int32_t(sample * 32768.f) * (1U << 16);
|
||||||
input_buffer_write(sample32);
|
input_buffer_write(sample32);
|
||||||
}
|
}
|
||||||
|
|
||||||
Error AudioDriverJavaScript::init() {
|
Error AudioDriverJavaScript::init() {
|
||||||
|
|
||||||
int mix_rate = GLOBAL_GET("audio/mix_rate");
|
int mix_rate = GLOBAL_GET("audio/mix_rate");
|
||||||
int latency = GLOBAL_GET("audio/output_latency");
|
int latency = GLOBAL_GET("audio/output_latency");
|
||||||
|
|
||||||
/* clang-format off */
|
/* clang-format off */
|
||||||
EM_ASM({
|
_driver_id = EM_ASM_INT({
|
||||||
const MIX_RATE = $0;
|
const MIX_RATE = $0;
|
||||||
const LATENCY = $1 / 1000;
|
const LATENCY = $1 / 1000;
|
||||||
_audioDriver_audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: MIX_RATE, latencyHint: LATENCY});
|
return Module.IDHandler.add({
|
||||||
_audioDriver_audioInput = null;
|
'context': new (window.AudioContext || window.webkitAudioContext)({ sampleRate: MIX_RATE, latencyHint: LATENCY}),
|
||||||
_audioDriver_inputStream = null;
|
'input': null,
|
||||||
_audioDriver_scriptNode = null;
|
'stream': null,
|
||||||
|
'script': null
|
||||||
|
});
|
||||||
}, mix_rate, latency);
|
}, mix_rate, latency);
|
||||||
/* clang-format on */
|
/* clang-format on */
|
||||||
|
|
||||||
|
@ -88,14 +84,16 @@ Error AudioDriverJavaScript::init() {
|
||||||
buffer_length = closest_power_of_2((latency * mix_rate / 1000) * channel_count);
|
buffer_length = closest_power_of_2((latency * mix_rate / 1000) * channel_count);
|
||||||
/* clang-format off */
|
/* clang-format off */
|
||||||
buffer_length = EM_ASM_INT({
|
buffer_length = EM_ASM_INT({
|
||||||
const BUFFER_LENGTH = $0;
|
var ref = Module.IDHandler.get($0);
|
||||||
const CHANNEL_COUNT = $1;
|
const ctx = ref['context'];
|
||||||
|
const BUFFER_LENGTH = $1;
|
||||||
|
const CHANNEL_COUNT = $2;
|
||||||
|
|
||||||
_audioDriver_scriptNode = _audioDriver_audioContext.createScriptProcessor(BUFFER_LENGTH, 2, CHANNEL_COUNT);
|
var script = ctx.createScriptProcessor(BUFFER_LENGTH, 2, CHANNEL_COUNT);
|
||||||
_audioDriver_scriptNode.connect(_audioDriver_audioContext.destination);
|
script.connect(ctx.destination);
|
||||||
|
ref['script'] = script;
|
||||||
return _audioDriver_scriptNode.bufferSize;
|
return script.bufferSize;
|
||||||
}, buffer_length, channel_count);
|
}, _driver_id, buffer_length, channel_count);
|
||||||
/* clang-format on */
|
/* clang-format on */
|
||||||
if (!buffer_length) {
|
if (!buffer_length) {
|
||||||
return FAILED;
|
return FAILED;
|
||||||
|
@ -111,14 +109,14 @@ Error AudioDriverJavaScript::init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioDriverJavaScript::start() {
|
void AudioDriverJavaScript::start() {
|
||||||
|
|
||||||
/* clang-format off */
|
/* clang-format off */
|
||||||
EM_ASM({
|
EM_ASM({
|
||||||
var INTERNAL_BUFFER_PTR = $0;
|
const ref = Module.IDHandler.get($0);
|
||||||
|
var INTERNAL_BUFFER_PTR = $1;
|
||||||
|
|
||||||
var audioDriverMixFunction = cwrap('audio_driver_js_mix');
|
var audioDriverMixFunction = cwrap('audio_driver_js_mix');
|
||||||
var audioDriverProcessCapture = cwrap('audio_driver_process_capture', null, ['number']);
|
var audioDriverProcessCapture = cwrap('audio_driver_process_capture', null, ['number']);
|
||||||
_audioDriver_scriptNode.onaudioprocess = function(audioProcessingEvent) {
|
ref['script'].onaudioprocess = function(audioProcessingEvent) {
|
||||||
audioDriverMixFunction();
|
audioDriverMixFunction();
|
||||||
|
|
||||||
var input = audioProcessingEvent.inputBuffer;
|
var input = audioProcessingEvent.inputBuffer;
|
||||||
|
@ -135,7 +133,7 @@ void AudioDriverJavaScript::start() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_audioDriver_audioInput) {
|
if (ref['input']) {
|
||||||
var inputDataL = input.getChannelData(0);
|
var inputDataL = input.getChannelData(0);
|
||||||
var inputDataR = input.getChannelData(1);
|
var inputDataR = input.getChannelData(1);
|
||||||
for (var i = 0; i < inputDataL.length; i++) {
|
for (var i = 0; i < inputDataL.length; i++) {
|
||||||
|
@ -144,51 +142,54 @@ void AudioDriverJavaScript::start() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, internal_buffer);
|
}, _driver_id, internal_buffer);
|
||||||
/* clang-format on */
|
/* clang-format on */
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioDriverJavaScript::resume() {
|
void AudioDriverJavaScript::resume() {
|
||||||
/* clang-format off */
|
/* clang-format off */
|
||||||
EM_ASM({
|
EM_ASM({
|
||||||
if (_audioDriver_audioContext.resume)
|
const ref = Module.IDHandler.get($0);
|
||||||
_audioDriver_audioContext.resume();
|
if (ref && ref['context'] && ref['context'].resume)
|
||||||
});
|
ref['context'].resume();
|
||||||
|
}, _driver_id);
|
||||||
/* clang-format on */
|
/* clang-format on */
|
||||||
}
|
}
|
||||||
|
|
||||||
float AudioDriverJavaScript::get_latency() {
|
float AudioDriverJavaScript::get_latency() {
|
||||||
/* clang-format off */
|
/* clang-format off */
|
||||||
return EM_ASM_DOUBLE({
|
return EM_ASM_DOUBLE({
|
||||||
|
const ref = Module.IDHandler.get($0);
|
||||||
var latency = 0;
|
var latency = 0;
|
||||||
if (_audioDriver_audioContext) {
|
if (ref && ref['context']) {
|
||||||
if (_audioDriver_audioContext.baseLatency) {
|
const ctx = ref['context'];
|
||||||
latency += _audioDriver_audioContext.baseLatency;
|
if (ctx.baseLatency) {
|
||||||
|
latency += ctx.baseLatency;
|
||||||
}
|
}
|
||||||
if (_audioDriver_audioContext.outputLatency) {
|
if (ctx.outputLatency) {
|
||||||
latency += _audioDriver_audioContext.outputLatency;
|
latency += ctx.outputLatency;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return latency;
|
return latency;
|
||||||
});
|
}, _driver_id);
|
||||||
/* clang-format on */
|
/* clang-format on */
|
||||||
}
|
}
|
||||||
|
|
||||||
int AudioDriverJavaScript::get_mix_rate() const {
|
int AudioDriverJavaScript::get_mix_rate() const {
|
||||||
|
|
||||||
/* clang-format off */
|
/* clang-format off */
|
||||||
return EM_ASM_INT_V({
|
return EM_ASM_INT({
|
||||||
return _audioDriver_audioContext.sampleRate;
|
const ref = Module.IDHandler.get($0);
|
||||||
});
|
return ref && ref['context'] ? ref['context'].sampleRate : 0;
|
||||||
|
}, _driver_id);
|
||||||
/* clang-format on */
|
/* clang-format on */
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioDriver::SpeakerMode AudioDriverJavaScript::get_speaker_mode() const {
|
AudioDriver::SpeakerMode AudioDriverJavaScript::get_speaker_mode() const {
|
||||||
|
|
||||||
/* clang-format off */
|
/* clang-format off */
|
||||||
return get_speaker_mode_by_total_channels(EM_ASM_INT_V({
|
return get_speaker_mode_by_total_channels(EM_ASM_INT({
|
||||||
return _audioDriver_audioContext.destination.channelCount;
|
const ref = Module.IDHandler.get($0);
|
||||||
}));
|
return ref && ref['context'] ? ref['context'].destination.channelCount : 0;
|
||||||
|
}, _driver_id));
|
||||||
/* clang-format on */
|
/* clang-format on */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,16 +200,38 @@ void AudioDriverJavaScript::lock() {
|
||||||
void AudioDriverJavaScript::unlock() {
|
void AudioDriverJavaScript::unlock() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioDriverJavaScript::finish() {
|
void AudioDriverJavaScript::finish_async() {
|
||||||
|
// Close the context, add the operation to the async_finish list in module.
|
||||||
|
int id = _driver_id;
|
||||||
|
_driver_id = 0;
|
||||||
|
|
||||||
/* clang-format off */
|
/* clang-format off */
|
||||||
EM_ASM({
|
EM_ASM({
|
||||||
_audioDriver_audioContext = null;
|
var ref = Module.IDHandler.get($0);
|
||||||
_audioDriver_audioInput = null;
|
Module.async_finish.push(new Promise(function(accept, reject) {
|
||||||
_audioDriver_scriptNode = null;
|
if (!ref) {
|
||||||
|
console.log("Ref not found!", $0, Module.IDHandler);
|
||||||
|
setTimeout(accept, 0);
|
||||||
|
} else {
|
||||||
|
const context = ref['context'];
|
||||||
|
// Disconnect script and input.
|
||||||
|
ref['script'].disconnect();
|
||||||
|
if (ref['input'])
|
||||||
|
ref['input'].disconnect();
|
||||||
|
ref = null;
|
||||||
|
context.close().then(function() {
|
||||||
|
accept();
|
||||||
|
}).catch(function(e) {
|
||||||
|
accept();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
Module.IDHandler.remove($0);
|
||||||
|
}, id);
|
||||||
/* clang-format on */
|
/* clang-format on */
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioDriverJavaScript::finish() {
|
||||||
if (internal_buffer) {
|
if (internal_buffer) {
|
||||||
memdelete_arr(internal_buffer);
|
memdelete_arr(internal_buffer);
|
||||||
internal_buffer = NULL;
|
internal_buffer = NULL;
|
||||||
|
@ -216,15 +239,15 @@ void AudioDriverJavaScript::finish() {
|
||||||
}
|
}
|
||||||
|
|
||||||
Error AudioDriverJavaScript::capture_start() {
|
Error AudioDriverJavaScript::capture_start() {
|
||||||
|
|
||||||
input_buffer_init(buffer_length);
|
input_buffer_init(buffer_length);
|
||||||
|
|
||||||
/* clang-format off */
|
/* clang-format off */
|
||||||
EM_ASM({
|
EM_ASM({
|
||||||
function gotMediaInput(stream) {
|
function gotMediaInput(stream) {
|
||||||
_audioDriver_inputStream = stream;
|
var ref = Module.IDHandler.get($0);
|
||||||
_audioDriver_audioInput = _audioDriver_audioContext.createMediaStreamSource(stream);
|
ref['stream'] = stream;
|
||||||
_audioDriver_audioInput.connect(_audioDriver_scriptNode);
|
ref['input'] = ref['context'].createMediaStreamSource(stream);
|
||||||
|
ref['input'].connect(ref['script']);
|
||||||
}
|
}
|
||||||
|
|
||||||
function gotMediaInputError(e) {
|
function gotMediaInputError(e) {
|
||||||
|
@ -238,30 +261,30 @@ Error AudioDriverJavaScript::capture_start() {
|
||||||
navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
|
navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
|
||||||
navigator.getUserMedia({"audio": true}, gotMediaInput, gotMediaInputError);
|
navigator.getUserMedia({"audio": true}, gotMediaInput, gotMediaInputError);
|
||||||
}
|
}
|
||||||
});
|
}, _driver_id);
|
||||||
/* clang-format on */
|
/* clang-format on */
|
||||||
|
|
||||||
return OK;
|
return OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
Error AudioDriverJavaScript::capture_stop() {
|
Error AudioDriverJavaScript::capture_stop() {
|
||||||
|
|
||||||
/* clang-format off */
|
/* clang-format off */
|
||||||
EM_ASM({
|
EM_ASM({
|
||||||
if (_audioDriver_inputStream) {
|
var ref = Module.IDHandler.get($0);
|
||||||
const tracks = _audioDriver_inputStream.getTracks();
|
if (ref['stream']) {
|
||||||
|
const tracks = ref['stream'].getTracks();
|
||||||
for (var i = 0; i < tracks.length; i++) {
|
for (var i = 0; i < tracks.length; i++) {
|
||||||
tracks[i].stop();
|
tracks[i].stop();
|
||||||
}
|
}
|
||||||
_audioDriver_inputStream = null;
|
ref['stream'] = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_audioDriver_audioInput) {
|
if (ref['input']) {
|
||||||
_audioDriver_audioInput.disconnect();
|
ref['input'].disconnect();
|
||||||
_audioDriver_audioInput = null;
|
ref['input'] = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
}, _driver_id);
|
||||||
/* clang-format on */
|
/* clang-format on */
|
||||||
|
|
||||||
input_buffer.clear();
|
input_buffer.clear();
|
||||||
|
@ -270,8 +293,9 @@ Error AudioDriverJavaScript::capture_stop() {
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioDriverJavaScript::AudioDriverJavaScript() {
|
AudioDriverJavaScript::AudioDriverJavaScript() {
|
||||||
|
_driver_id = 0;
|
||||||
internal_buffer = NULL;
|
internal_buffer = NULL;
|
||||||
|
buffer_length = 0;
|
||||||
|
|
||||||
singleton = this;
|
singleton = this;
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,7 @@ class AudioDriverJavaScript : public AudioDriver {
|
||||||
|
|
||||||
float *internal_buffer;
|
float *internal_buffer;
|
||||||
|
|
||||||
|
int _driver_id;
|
||||||
int buffer_length;
|
int buffer_length;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
@ -56,6 +57,7 @@ public:
|
||||||
virtual void lock();
|
virtual void lock();
|
||||||
virtual void unlock();
|
virtual void unlock();
|
||||||
virtual void finish();
|
virtual void finish();
|
||||||
|
void finish_async();
|
||||||
|
|
||||||
virtual Error capture_start();
|
virtual Error capture_start();
|
||||||
virtual Error capture_stop();
|
virtual Error capture_stop();
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from emscripten_helpers import parse_config, run_closure_compiler, create_engine_file
|
||||||
|
|
||||||
|
|
||||||
def is_active():
|
def is_active():
|
||||||
return True
|
return True
|
||||||
|
@ -19,6 +21,8 @@ def get_opts():
|
||||||
return [
|
return [
|
||||||
# eval() can be a security concern, so it can be disabled.
|
# eval() can be a security concern, so it can be disabled.
|
||||||
BoolVariable("javascript_eval", "Enable JavaScript eval interface", True),
|
BoolVariable("javascript_eval", "Enable JavaScript eval interface", True),
|
||||||
|
BoolVariable("threads_enabled", "Enable WebAssembly Threads support (limited browser support)", False),
|
||||||
|
BoolVariable("use_closure_compiler", "Use closure compiler to minimize JavaScript code", False),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,7 +42,7 @@ def configure(env):
|
||||||
|
|
||||||
## Build type
|
## Build type
|
||||||
|
|
||||||
if env["target"] != "debug":
|
if env["target"] == "release":
|
||||||
# Use -Os to prioritize optimizing for reduced file size. This is
|
# Use -Os to prioritize optimizing for reduced file size. This is
|
||||||
# particularly valuable for the web platform because it directly
|
# particularly valuable for the web platform because it directly
|
||||||
# decreases download time.
|
# decreases download time.
|
||||||
|
@ -47,40 +51,57 @@ def configure(env):
|
||||||
# run-time performance.
|
# run-time performance.
|
||||||
env.Append(CCFLAGS=["-Os"])
|
env.Append(CCFLAGS=["-Os"])
|
||||||
env.Append(LINKFLAGS=["-Os"])
|
env.Append(LINKFLAGS=["-Os"])
|
||||||
if env["target"] == "release_debug":
|
elif env["target"] == "release_debug":
|
||||||
|
env.Append(CCFLAGS=["-Os"])
|
||||||
|
env.Append(LINKFLAGS=["-Os"])
|
||||||
env.Append(CPPDEFINES=["DEBUG_ENABLED"])
|
env.Append(CPPDEFINES=["DEBUG_ENABLED"])
|
||||||
# Retain function names for backtraces at the cost of file size.
|
# Retain function names for backtraces at the cost of file size.
|
||||||
env.Append(LINKFLAGS=["--profiling-funcs"])
|
env.Append(LINKFLAGS=["--profiling-funcs"])
|
||||||
else:
|
else: # "debug"
|
||||||
env.Append(CPPDEFINES=["DEBUG_ENABLED"])
|
env.Append(CPPDEFINES=["DEBUG_ENABLED"])
|
||||||
env.Append(CCFLAGS=["-O1", "-g"])
|
env.Append(CCFLAGS=["-O1", "-g"])
|
||||||
env.Append(LINKFLAGS=["-O1", "-g"])
|
env.Append(LINKFLAGS=["-O1", "-g"])
|
||||||
env.Append(LINKFLAGS=["-s", "ASSERTIONS=1"])
|
env.Append(LINKFLAGS=["-s", "ASSERTIONS=1"])
|
||||||
|
|
||||||
## Compiler configuration
|
if env["tools"]:
|
||||||
|
if not env["threads_enabled"]:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Threads must be enabled to build the editor. Please add the 'threads_enabled=yes' option"
|
||||||
|
)
|
||||||
|
# Tools need more memory. Initial stack memory in bytes. See `src/settings.js` in emscripten repository (will be renamed to INITIAL_MEMORY).
|
||||||
|
env.Append(LINKFLAGS=["-s", "TOTAL_MEMORY=33554432"])
|
||||||
|
else:
|
||||||
|
# Disable exceptions and rtti on non-tools (template) builds
|
||||||
|
# These flags help keep the file size down.
|
||||||
|
env.Append(CCFLAGS=["-fno-exceptions", "-fno-rtti"])
|
||||||
|
# Don't use dynamic_cast, necessary with no-rtti.
|
||||||
|
env.Append(CPPDEFINES=["NO_SAFE_CAST"])
|
||||||
|
|
||||||
|
## Copy env variables.
|
||||||
env["ENV"] = os.environ
|
env["ENV"] = os.environ
|
||||||
|
|
||||||
em_config_file = os.getenv("EM_CONFIG") or os.path.expanduser("~/.emscripten")
|
# LTO
|
||||||
if not os.path.exists(em_config_file):
|
if env["use_lto"]:
|
||||||
raise RuntimeError("Emscripten configuration file '%s' does not exist" % em_config_file)
|
env.Append(CCFLAGS=["-s", "WASM_OBJECT_FILES=0"])
|
||||||
with open(em_config_file) as f:
|
env.Append(LINKFLAGS=["-s", "WASM_OBJECT_FILES=0"])
|
||||||
em_config = {}
|
env.Append(LINKFLAGS=["--llvm-lto", "1"])
|
||||||
try:
|
|
||||||
# Emscripten configuration file is a Python file with simple assignments.
|
# Closure compiler
|
||||||
exec(f.read(), em_config)
|
if env["use_closure_compiler"]:
|
||||||
except StandardError as e:
|
# For emscripten support code.
|
||||||
raise RuntimeError("Emscripten configuration file '%s' is invalid:\n%s" % (em_config_file, e))
|
env.Append(LINKFLAGS=["--closure", "1"])
|
||||||
if "BINARYEN_ROOT" in em_config and os.path.isdir(os.path.join(em_config.get("BINARYEN_ROOT"), "emscripten")):
|
# Register builder for our Engine files
|
||||||
# New style, emscripten path as a subfolder of BINARYEN_ROOT
|
jscc = env.Builder(generator=run_closure_compiler, suffix=".cc.js", src_suffix=".js")
|
||||||
env.PrependENVPath("PATH", os.path.join(em_config.get("BINARYEN_ROOT"), "emscripten"))
|
env.Append(BUILDERS={"BuildJS": jscc})
|
||||||
elif "EMSCRIPTEN_ROOT" in em_config:
|
|
||||||
# Old style (but can be there as a result from previous activation, so do last)
|
# Add method that joins/compiles our Engine files.
|
||||||
env.PrependENVPath("PATH", em_config.get("EMSCRIPTEN_ROOT"))
|
env.AddMethod(create_engine_file, "CreateEngineFile")
|
||||||
else:
|
|
||||||
raise RuntimeError(
|
# Closure compiler extern and support for ecmascript specs (const, let, etc).
|
||||||
"'BINARYEN_ROOT' or 'EMSCRIPTEN_ROOT' missing in Emscripten configuration file '%s'" % em_config_file
|
env["ENV"]["EMCC_CLOSURE_ARGS"] = "--language_in ECMASCRIPT6"
|
||||||
)
|
|
||||||
|
em_config = parse_config()
|
||||||
|
env.PrependENVPath("PATH", em_config["EMCC_ROOT"])
|
||||||
|
|
||||||
env["CC"] = "emcc"
|
env["CC"] = "emcc"
|
||||||
env["CXX"] = "em++"
|
env["CXX"] = "em++"
|
||||||
|
@ -105,44 +126,31 @@ def configure(env):
|
||||||
env["LIBPREFIXES"] = ["$LIBPREFIX"]
|
env["LIBPREFIXES"] = ["$LIBPREFIX"]
|
||||||
env["LIBSUFFIXES"] = ["$LIBSUFFIX"]
|
env["LIBSUFFIXES"] = ["$LIBSUFFIX"]
|
||||||
|
|
||||||
## Compile flags
|
|
||||||
|
|
||||||
env.Prepend(CPPPATH=["#platform/javascript"])
|
env.Prepend(CPPPATH=["#platform/javascript"])
|
||||||
env.Append(CPPDEFINES=["JAVASCRIPT_ENABLED", "UNIX_ENABLED"])
|
env.Append(CPPDEFINES=["JAVASCRIPT_ENABLED", "UNIX_ENABLED"])
|
||||||
|
|
||||||
# No multi-threading (SharedArrayBuffer) available yet,
|
|
||||||
# once feasible also consider memory buffer size issues.
|
|
||||||
env.Append(CPPDEFINES=["NO_THREADS"])
|
|
||||||
|
|
||||||
# Disable exceptions and rtti on non-tools (template) builds
|
|
||||||
if not env["tools"]:
|
|
||||||
# These flags help keep the file size down.
|
|
||||||
env.Append(CCFLAGS=["-fno-exceptions", "-fno-rtti"])
|
|
||||||
# Don't use dynamic_cast, necessary with no-rtti.
|
|
||||||
env.Append(CPPDEFINES=["NO_SAFE_CAST"])
|
|
||||||
|
|
||||||
if env["javascript_eval"]:
|
if env["javascript_eval"]:
|
||||||
env.Append(CPPDEFINES=["JAVASCRIPT_EVAL_ENABLED"])
|
env.Append(CPPDEFINES=["JAVASCRIPT_EVAL_ENABLED"])
|
||||||
|
|
||||||
## Link flags
|
# Thread support (via SharedArrayBuffer).
|
||||||
|
if env["threads_enabled"]:
|
||||||
|
env.Append(CPPDEFINES=["PTHREAD_NO_RENAME"])
|
||||||
|
env.Append(CCFLAGS=["-s", "USE_PTHREADS=1"])
|
||||||
|
env.Append(LINKFLAGS=["-s", "USE_PTHREADS=1"])
|
||||||
|
env.Append(LINKFLAGS=["-s", "PTHREAD_POOL_SIZE=4"])
|
||||||
|
env.Append(LINKFLAGS=["-s", "WASM_MEM_MAX=2048MB"])
|
||||||
|
else:
|
||||||
|
env.Append(CPPDEFINES=["NO_THREADS"])
|
||||||
|
|
||||||
|
# Reduce code size by generating less support code (e.g. skip NodeJS support).
|
||||||
|
env.Append(LINKFLAGS=["-s", "ENVIRONMENT=web,worker"])
|
||||||
|
|
||||||
# We use IDBFS in javascript_main.cpp. Since Emscripten 1.39.1 it needs to
|
# We use IDBFS in javascript_main.cpp. Since Emscripten 1.39.1 it needs to
|
||||||
# be linked explicitly.
|
# be linked explicitly.
|
||||||
env.Append(LIBS=["idbfs.js"])
|
env.Append(LIBS=["idbfs.js"])
|
||||||
|
|
||||||
env.Append(LINKFLAGS=["-s", "BINARYEN=1"])
|
env.Append(LINKFLAGS=["-s", "BINARYEN=1"])
|
||||||
|
env.Append(LINKFLAGS=["-s", "MODULARIZE=1", "-s", "EXPORT_NAME='Godot'"])
|
||||||
# Only include the JavaScript support code for the web environment
|
|
||||||
# (i.e. exclude Node.js and other unused environments).
|
|
||||||
# This makes the JavaScript support code about 4 KB smaller.
|
|
||||||
env.Append(LINKFLAGS=["-s", "ENVIRONMENT=web"])
|
|
||||||
|
|
||||||
# This needs to be defined for Emscripten using 'fastcomp' (default pre-1.39.0)
|
|
||||||
# and undefined if using 'upstream'. And to make things simple, earlier
|
|
||||||
# Emscripten versions didn't include 'fastcomp' in their path, so we check
|
|
||||||
# against the presence of 'upstream' to conditionally add the flag.
|
|
||||||
if not "upstream" in em_config["EMSCRIPTEN_ROOT"]:
|
|
||||||
env.Append(LINKFLAGS=["-s", "BINARYEN_TRAP_MODE='clamp'"])
|
|
||||||
|
|
||||||
# Allow increasing memory buffer size during runtime. This is efficient
|
# Allow increasing memory buffer size during runtime. This is efficient
|
||||||
# when using WebAssembly (in comparison to asm.js) and works well for
|
# when using WebAssembly (in comparison to asm.js) and works well for
|
||||||
|
@ -154,8 +162,10 @@ def configure(env):
|
||||||
|
|
||||||
env.Append(LINKFLAGS=["-s", "INVOKE_RUN=0"])
|
env.Append(LINKFLAGS=["-s", "INVOKE_RUN=0"])
|
||||||
|
|
||||||
# TODO: Reevaluate usage of this setting now that engine.js manages engine runtime.
|
# Allow use to take control of swapping WebGL buffers.
|
||||||
env.Append(LINKFLAGS=["-s", "NO_EXIT_RUNTIME=1"])
|
env.Append(LINKFLAGS=["-s", "OFFSCREEN_FRAMEBUFFER=1"])
|
||||||
|
|
||||||
# adding flag due to issue with emscripten 1.38.41 callMain method https://github.com/emscripten-core/emscripten/blob/incoming/ChangeLog.md#v13841-08072019
|
# callMain for manual start, FS for preloading, PATH and ERRNO_CODES for BrowserFS.
|
||||||
env.Append(LINKFLAGS=["-s", 'EXTRA_EXPORTED_RUNTIME_METHODS=["callMain"]'])
|
env.Append(LINKFLAGS=["-s", "EXTRA_EXPORTED_RUNTIME_METHODS=['callMain', 'FS']"])
|
||||||
|
# Add code that allow exiting runtime.
|
||||||
|
env.Append(LINKFLAGS=["-s", "EXIT_RUNTIME=1"])
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def parse_config():
|
||||||
|
em_config_file = os.getenv("EM_CONFIG") or os.path.expanduser("~/.emscripten")
|
||||||
|
if not os.path.exists(em_config_file):
|
||||||
|
raise RuntimeError("Emscripten configuration file '%s' does not exist" % em_config_file)
|
||||||
|
|
||||||
|
normalized = {}
|
||||||
|
em_config = {}
|
||||||
|
with open(em_config_file) as f:
|
||||||
|
try:
|
||||||
|
# Emscripten configuration file is a Python file with simple assignments.
|
||||||
|
exec(f.read(), em_config)
|
||||||
|
except StandardError as e:
|
||||||
|
raise RuntimeError("Emscripten configuration file '%s' is invalid:\n%s" % (em_config_file, e))
|
||||||
|
normalized["EMCC_ROOT"] = em_config.get("EMSCRIPTEN_ROOT")
|
||||||
|
normalized["NODE_JS"] = em_config.get("NODE_JS")
|
||||||
|
normalized["CLOSURE_BIN"] = os.path.join(normalized["EMCC_ROOT"], "node_modules", ".bin", "google-closure-compiler")
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def run_closure_compiler(target, source, env, for_signature):
|
||||||
|
cfg = parse_config()
|
||||||
|
cmd = [cfg["NODE_JS"], cfg["CLOSURE_BIN"]]
|
||||||
|
cmd.extend(["--compilation_level", "ADVANCED_OPTIMIZATIONS"])
|
||||||
|
for f in env["JSEXTERNS"]:
|
||||||
|
cmd.extend(["--externs", f.get_abspath()])
|
||||||
|
for f in source:
|
||||||
|
cmd.extend(["--js", f.get_abspath()])
|
||||||
|
cmd.extend(["--js_output_file", target[0].get_abspath()])
|
||||||
|
return " ".join(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def create_engine_file(env, target, source, externs):
|
||||||
|
if env["use_closure_compiler"]:
|
||||||
|
return env.BuildJS(target, source, JSEXTERNS=externs)
|
||||||
|
return env.Textfile(target, [env.File(s) for s in source])
|
|
@ -1,411 +0,0 @@
|
||||||
// The following is concatenated with generated code, and acts as the end
|
|
||||||
// of a wrapper for said code. See pre.js for the other part of the
|
|
||||||
// wrapper.
|
|
||||||
exposedLibs['PATH'] = PATH;
|
|
||||||
exposedLibs['FS'] = FS;
|
|
||||||
return Module;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
(function() {
|
|
||||||
var engine = Engine;
|
|
||||||
|
|
||||||
var DOWNLOAD_ATTEMPTS_MAX = 4;
|
|
||||||
|
|
||||||
var basePath = null;
|
|
||||||
var wasmFilenameExtensionOverride = null;
|
|
||||||
var engineLoadPromise = null;
|
|
||||||
|
|
||||||
var loadingFiles = {};
|
|
||||||
|
|
||||||
function getPathLeaf(path) {
|
|
||||||
|
|
||||||
while (path.endsWith('/'))
|
|
||||||
path = path.slice(0, -1);
|
|
||||||
return path.slice(path.lastIndexOf('/') + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBasePath(path) {
|
|
||||||
|
|
||||||
if (path.endsWith('/'))
|
|
||||||
path = path.slice(0, -1);
|
|
||||||
if (path.lastIndexOf('.') > path.lastIndexOf('/'))
|
|
||||||
path = path.slice(0, path.lastIndexOf('.'));
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBaseName(path) {
|
|
||||||
|
|
||||||
return getPathLeaf(getBasePath(path));
|
|
||||||
}
|
|
||||||
|
|
||||||
Engine = function Engine() {
|
|
||||||
|
|
||||||
this.rtenv = null;
|
|
||||||
|
|
||||||
var LIBS = {};
|
|
||||||
|
|
||||||
var initPromise = null;
|
|
||||||
var unloadAfterInit = true;
|
|
||||||
|
|
||||||
var preloadedFiles = [];
|
|
||||||
|
|
||||||
var resizeCanvasOnStart = true;
|
|
||||||
var progressFunc = null;
|
|
||||||
var preloadProgressTracker = {};
|
|
||||||
var lastProgress = { loaded: 0, total: 0 };
|
|
||||||
|
|
||||||
var canvas = null;
|
|
||||||
var executableName = null;
|
|
||||||
var locale = null;
|
|
||||||
var stdout = null;
|
|
||||||
var stderr = null;
|
|
||||||
|
|
||||||
this.init = function(newBasePath) {
|
|
||||||
|
|
||||||
if (!initPromise) {
|
|
||||||
initPromise = Engine.load(newBasePath).then(
|
|
||||||
instantiate.bind(this)
|
|
||||||
);
|
|
||||||
requestAnimationFrame(animateProgress);
|
|
||||||
if (unloadAfterInit)
|
|
||||||
initPromise.then(Engine.unloadEngine);
|
|
||||||
}
|
|
||||||
return initPromise;
|
|
||||||
};
|
|
||||||
|
|
||||||
function instantiate(wasmBuf) {
|
|
||||||
|
|
||||||
var rtenvProps = {
|
|
||||||
engine: this,
|
|
||||||
ENV: {},
|
|
||||||
};
|
|
||||||
if (typeof stdout === 'function')
|
|
||||||
rtenvProps.print = stdout;
|
|
||||||
if (typeof stderr === 'function')
|
|
||||||
rtenvProps.printErr = stderr;
|
|
||||||
rtenvProps.instantiateWasm = function(imports, onSuccess) {
|
|
||||||
WebAssembly.instantiate(wasmBuf, imports).then(function(result) {
|
|
||||||
onSuccess(result.instance);
|
|
||||||
});
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Promise(function(resolve, reject) {
|
|
||||||
rtenvProps.onRuntimeInitialized = resolve;
|
|
||||||
rtenvProps.onAbort = reject;
|
|
||||||
rtenvProps.thisProgram = executableName;
|
|
||||||
rtenvProps.engine.rtenv = Engine.RuntimeEnvironment(rtenvProps, LIBS);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.preloadFile = function(pathOrBuffer, destPath) {
|
|
||||||
|
|
||||||
if (pathOrBuffer instanceof ArrayBuffer) {
|
|
||||||
pathOrBuffer = new Uint8Array(pathOrBuffer);
|
|
||||||
} else if (ArrayBuffer.isView(pathOrBuffer)) {
|
|
||||||
pathOrBuffer = new Uint8Array(pathOrBuffer.buffer);
|
|
||||||
}
|
|
||||||
if (pathOrBuffer instanceof Uint8Array) {
|
|
||||||
preloadedFiles.push({
|
|
||||||
path: destPath,
|
|
||||||
buffer: pathOrBuffer
|
|
||||||
});
|
|
||||||
return Promise.resolve();
|
|
||||||
} else if (typeof pathOrBuffer === 'string') {
|
|
||||||
return loadPromise(pathOrBuffer, preloadProgressTracker).then(function(xhr) {
|
|
||||||
preloadedFiles.push({
|
|
||||||
path: destPath || pathOrBuffer,
|
|
||||||
buffer: xhr.response
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw Promise.reject("Invalid object for preloading");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.start = function() {
|
|
||||||
|
|
||||||
return this.init().then(
|
|
||||||
Function.prototype.apply.bind(synchronousStart, this, arguments)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.startGame = function(execName, mainPack) {
|
|
||||||
|
|
||||||
executableName = execName;
|
|
||||||
var mainArgs = [ '--main-pack', getPathLeaf(mainPack) ];
|
|
||||||
|
|
||||||
return Promise.all([
|
|
||||||
this.init(getBasePath(execName)),
|
|
||||||
this.preloadFile(mainPack, getPathLeaf(mainPack))
|
|
||||||
]).then(
|
|
||||||
Function.prototype.apply.bind(synchronousStart, this, mainArgs)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function synchronousStart() {
|
|
||||||
|
|
||||||
if (canvas instanceof HTMLCanvasElement) {
|
|
||||||
this.rtenv.canvas = canvas;
|
|
||||||
} else {
|
|
||||||
var firstCanvas = document.getElementsByTagName('canvas')[0];
|
|
||||||
if (firstCanvas instanceof HTMLCanvasElement) {
|
|
||||||
this.rtenv.canvas = firstCanvas;
|
|
||||||
} else {
|
|
||||||
throw new Error("No canvas found");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var actualCanvas = this.rtenv.canvas;
|
|
||||||
// canvas can grab focus on click
|
|
||||||
if (actualCanvas.tabIndex < 0) {
|
|
||||||
actualCanvas.tabIndex = 0;
|
|
||||||
}
|
|
||||||
// necessary to calculate cursor coordinates correctly
|
|
||||||
actualCanvas.style.padding = 0;
|
|
||||||
actualCanvas.style.borderWidth = 0;
|
|
||||||
actualCanvas.style.borderStyle = 'none';
|
|
||||||
// disable right-click context menu
|
|
||||||
actualCanvas.addEventListener('contextmenu', function(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
}, false);
|
|
||||||
// until context restoration is implemented
|
|
||||||
actualCanvas.addEventListener('webglcontextlost', function(ev) {
|
|
||||||
alert("WebGL context lost, please reload the page");
|
|
||||||
ev.preventDefault();
|
|
||||||
}, false);
|
|
||||||
|
|
||||||
if (locale) {
|
|
||||||
this.rtenv.locale = locale;
|
|
||||||
} else {
|
|
||||||
this.rtenv.locale = navigator.languages ? navigator.languages[0] : navigator.language;
|
|
||||||
}
|
|
||||||
this.rtenv.locale = this.rtenv.locale.split('.')[0];
|
|
||||||
this.rtenv.resizeCanvasOnStart = resizeCanvasOnStart;
|
|
||||||
|
|
||||||
preloadedFiles.forEach(function(file) {
|
|
||||||
var dir = LIBS.PATH.dirname(file.path);
|
|
||||||
try {
|
|
||||||
LIBS.FS.stat(dir);
|
|
||||||
} catch (e) {
|
|
||||||
if (e.code !== 'ENOENT') {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
LIBS.FS.mkdirTree(dir);
|
|
||||||
}
|
|
||||||
// With memory growth, canOwn should be false.
|
|
||||||
LIBS.FS.createDataFile(file.path, null, new Uint8Array(file.buffer), true, true, false);
|
|
||||||
}, this);
|
|
||||||
|
|
||||||
preloadedFiles = null;
|
|
||||||
initPromise = null;
|
|
||||||
this.rtenv.callMain(arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setProgressFunc = function(func) {
|
|
||||||
progressFunc = func;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setResizeCanvasOnStart = function(enabled) {
|
|
||||||
resizeCanvasOnStart = enabled;
|
|
||||||
};
|
|
||||||
|
|
||||||
function animateProgress() {
|
|
||||||
|
|
||||||
var loaded = 0;
|
|
||||||
var total = 0;
|
|
||||||
var totalIsValid = true;
|
|
||||||
var progressIsFinal = true;
|
|
||||||
|
|
||||||
[loadingFiles, preloadProgressTracker].forEach(function(tracker) {
|
|
||||||
Object.keys(tracker).forEach(function(file) {
|
|
||||||
if (!tracker[file].final)
|
|
||||||
progressIsFinal = false;
|
|
||||||
if (!totalIsValid || tracker[file].total === 0) {
|
|
||||||
totalIsValid = false;
|
|
||||||
total = 0;
|
|
||||||
} else {
|
|
||||||
total += tracker[file].total;
|
|
||||||
}
|
|
||||||
loaded += tracker[file].loaded;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
if (loaded !== lastProgress.loaded || total !== lastProgress.total) {
|
|
||||||
lastProgress.loaded = loaded;
|
|
||||||
lastProgress.total = total;
|
|
||||||
if (typeof progressFunc === 'function')
|
|
||||||
progressFunc(loaded, total);
|
|
||||||
}
|
|
||||||
if (!progressIsFinal)
|
|
||||||
requestAnimationFrame(animateProgress);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setCanvas = function(elem) {
|
|
||||||
canvas = elem;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setExecutableName = function(newName) {
|
|
||||||
|
|
||||||
executableName = newName;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setLocale = function(newLocale) {
|
|
||||||
|
|
||||||
locale = newLocale;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setUnloadAfterInit = function(enabled) {
|
|
||||||
|
|
||||||
if (enabled && !unloadAfterInit && initPromise) {
|
|
||||||
initPromise.then(Engine.unloadEngine);
|
|
||||||
}
|
|
||||||
unloadAfterInit = enabled;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setStdoutFunc = function(func) {
|
|
||||||
|
|
||||||
var print = function(text) {
|
|
||||||
if (arguments.length > 1) {
|
|
||||||
text = Array.prototype.slice.call(arguments).join(" ");
|
|
||||||
}
|
|
||||||
func(text);
|
|
||||||
};
|
|
||||||
if (this.rtenv)
|
|
||||||
this.rtenv.print = print;
|
|
||||||
stdout = print;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setStderrFunc = function(func) {
|
|
||||||
|
|
||||||
var printErr = function(text) {
|
|
||||||
if (arguments.length > 1)
|
|
||||||
text = Array.prototype.slice.call(arguments).join(" ");
|
|
||||||
func(text);
|
|
||||||
};
|
|
||||||
if (this.rtenv)
|
|
||||||
this.rtenv.printErr = printErr;
|
|
||||||
stderr = printErr;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
}; // Engine()
|
|
||||||
|
|
||||||
Engine.RuntimeEnvironment = engine.RuntimeEnvironment;
|
|
||||||
|
|
||||||
Engine.isWebGLAvailable = function(majorVersion = 1) {
|
|
||||||
|
|
||||||
var testContext = false;
|
|
||||||
try {
|
|
||||||
var testCanvas = document.createElement('canvas');
|
|
||||||
if (majorVersion === 1) {
|
|
||||||
testContext = testCanvas.getContext('webgl') || testCanvas.getContext('experimental-webgl');
|
|
||||||
} else if (majorVersion === 2) {
|
|
||||||
testContext = testCanvas.getContext('webgl2') || testCanvas.getContext('experimental-webgl2');
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
return !!testContext;
|
|
||||||
};
|
|
||||||
|
|
||||||
Engine.setWebAssemblyFilenameExtension = function(override) {
|
|
||||||
|
|
||||||
if (String(override).length === 0) {
|
|
||||||
throw new Error('Invalid WebAssembly filename extension override');
|
|
||||||
}
|
|
||||||
wasmFilenameExtensionOverride = String(override);
|
|
||||||
}
|
|
||||||
|
|
||||||
Engine.load = function(newBasePath) {
|
|
||||||
|
|
||||||
if (newBasePath !== undefined) basePath = getBasePath(newBasePath);
|
|
||||||
if (engineLoadPromise === null) {
|
|
||||||
if (typeof WebAssembly !== 'object')
|
|
||||||
return Promise.reject(new Error("Browser doesn't support WebAssembly"));
|
|
||||||
// TODO cache/retrieve module to/from idb
|
|
||||||
engineLoadPromise = loadPromise(basePath + '.' + (wasmFilenameExtensionOverride || 'wasm')).then(function(xhr) {
|
|
||||||
return xhr.response;
|
|
||||||
});
|
|
||||||
engineLoadPromise = engineLoadPromise.catch(function(err) {
|
|
||||||
engineLoadPromise = null;
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return engineLoadPromise;
|
|
||||||
};
|
|
||||||
|
|
||||||
Engine.unload = function() {
|
|
||||||
engineLoadPromise = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
function loadPromise(file, tracker) {
|
|
||||||
if (tracker === undefined)
|
|
||||||
tracker = loadingFiles;
|
|
||||||
return new Promise(function(resolve, reject) {
|
|
||||||
loadXHR(resolve, reject, file, tracker);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadXHR(resolve, reject, file, tracker) {
|
|
||||||
|
|
||||||
var xhr = new XMLHttpRequest;
|
|
||||||
xhr.open('GET', file);
|
|
||||||
if (!file.endsWith('.js')) {
|
|
||||||
xhr.responseType = 'arraybuffer';
|
|
||||||
}
|
|
||||||
['loadstart', 'progress', 'load', 'error', 'abort'].forEach(function(ev) {
|
|
||||||
xhr.addEventListener(ev, onXHREvent.bind(xhr, resolve, reject, file, tracker));
|
|
||||||
});
|
|
||||||
xhr.send();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onXHREvent(resolve, reject, file, tracker, ev) {
|
|
||||||
|
|
||||||
if (this.status >= 400) {
|
|
||||||
|
|
||||||
if (this.status < 500 || ++tracker[file].attempts >= DOWNLOAD_ATTEMPTS_MAX) {
|
|
||||||
reject(new Error("Failed loading file '" + file + "': " + this.statusText));
|
|
||||||
this.abort();
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
setTimeout(loadXHR.bind(null, resolve, reject, file, tracker), 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (ev.type) {
|
|
||||||
case 'loadstart':
|
|
||||||
if (tracker[file] === undefined) {
|
|
||||||
tracker[file] = {
|
|
||||||
total: ev.total,
|
|
||||||
loaded: ev.loaded,
|
|
||||||
attempts: 0,
|
|
||||||
final: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'progress':
|
|
||||||
tracker[file].loaded = ev.loaded;
|
|
||||||
tracker[file].total = ev.total;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'load':
|
|
||||||
tracker[file].final = true;
|
|
||||||
resolve(this);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'error':
|
|
||||||
if (++tracker[file].attempts >= DOWNLOAD_ATTEMPTS_MAX) {
|
|
||||||
tracker[file].final = true;
|
|
||||||
reject(new Error("Failed loading file '" + file + "'"));
|
|
||||||
} else {
|
|
||||||
setTimeout(loadXHR.bind(null, resolve, reject, file, tracker), 1000);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'abort':
|
|
||||||
tracker[file].final = true;
|
|
||||||
reject(new Error("Loading file '" + file + "' was aborted."));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
|
@ -0,0 +1,245 @@
|
||||||
|
Function('return this')()['Engine'] = (function() {
|
||||||
|
var preloader = new Preloader();
|
||||||
|
|
||||||
|
var wasmExt = '.wasm';
|
||||||
|
var unloadAfterInit = true;
|
||||||
|
var loadPath = '';
|
||||||
|
var loadPromise = null;
|
||||||
|
var initPromise = null;
|
||||||
|
var stderr = null;
|
||||||
|
var stdout = null;
|
||||||
|
var progressFunc = null;
|
||||||
|
|
||||||
|
function load(basePath) {
|
||||||
|
if (loadPromise == null) {
|
||||||
|
loadPath = basePath;
|
||||||
|
loadPromise = preloader.loadPromise(basePath + wasmExt);
|
||||||
|
preloader.setProgressFunc(progressFunc);
|
||||||
|
requestAnimationFrame(preloader.animateProgress);
|
||||||
|
}
|
||||||
|
return loadPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
function unload() {
|
||||||
|
loadPromise = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @constructor */
|
||||||
|
function Engine() {
|
||||||
|
this.canvas = null;
|
||||||
|
this.executableName = '';
|
||||||
|
this.rtenv = null;
|
||||||
|
this.customLocale = null;
|
||||||
|
this.resizeCanvasOnStart = false;
|
||||||
|
this.onExecute = null;
|
||||||
|
this.onExit = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
Engine.prototype.init = /** @param {string=} basePath */ function(basePath) {
|
||||||
|
if (initPromise) {
|
||||||
|
return initPromise;
|
||||||
|
}
|
||||||
|
if (loadPromise == null) {
|
||||||
|
if (!basePath) {
|
||||||
|
initPromise = Promise.reject(new Error("A base path must be provided when calling `init` and the engine is not loaded."));
|
||||||
|
return initPromise;
|
||||||
|
}
|
||||||
|
load(basePath);
|
||||||
|
}
|
||||||
|
var config = {};
|
||||||
|
if (typeof stdout === 'function')
|
||||||
|
config.print = stdout;
|
||||||
|
if (typeof stderr === 'function')
|
||||||
|
config.printErr = stderr;
|
||||||
|
var me = this;
|
||||||
|
initPromise = new Promise(function(resolve, reject) {
|
||||||
|
config['locateFile'] = Utils.createLocateRewrite(loadPath);
|
||||||
|
config['instantiateWasm'] = Utils.createInstantiatePromise(loadPromise);
|
||||||
|
Godot(config).then(function(module) {
|
||||||
|
me.rtenv = module;
|
||||||
|
if (unloadAfterInit) {
|
||||||
|
unload();
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
config = null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return initPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {function(string, string):Object} */
|
||||||
|
Engine.prototype.preloadFile = function(file, path) {
|
||||||
|
return preloader.preload(file, path);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {function(...string):Object} */
|
||||||
|
Engine.prototype.start = function() {
|
||||||
|
// Start from arguments.
|
||||||
|
var args = [];
|
||||||
|
for (var i = 0; i < arguments.length; i++) {
|
||||||
|
args.push(arguments[i]);
|
||||||
|
}
|
||||||
|
var me = this;
|
||||||
|
return me.init().then(function() {
|
||||||
|
if (!me.rtenv) {
|
||||||
|
return Promise.reject(new Error('The engine must be initialized before it can be started'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(me.canvas instanceof HTMLCanvasElement)) {
|
||||||
|
me.canvas = Utils.findCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canvas can grab focus on click, or key events won't work.
|
||||||
|
if (me.canvas.tabIndex < 0) {
|
||||||
|
me.canvas.tabIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable right-click context menu.
|
||||||
|
me.canvas.addEventListener('contextmenu', function(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
// Until context restoration is implemented warn the user of context loss.
|
||||||
|
me.canvas.addEventListener('webglcontextlost', function(ev) {
|
||||||
|
alert("WebGL context lost, please reload the page");
|
||||||
|
ev.preventDefault();
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
// Browser locale, or custom one if defined.
|
||||||
|
var locale = me.customLocale;
|
||||||
|
if (!locale) {
|
||||||
|
locale = navigator.languages ? navigator.languages[0] : navigator.language;
|
||||||
|
locale = locale.split('.')[0];
|
||||||
|
}
|
||||||
|
me.rtenv['locale'] = locale;
|
||||||
|
me.rtenv['canvas'] = me.canvas;
|
||||||
|
me.rtenv['thisProgram'] = me.executableName;
|
||||||
|
me.rtenv['resizeCanvasOnStart'] = me.resizeCanvasOnStart;
|
||||||
|
me.rtenv['noExitRuntime'] = true;
|
||||||
|
me.rtenv['onExecute'] = me.onExecute;
|
||||||
|
me.rtenv['onExit'] = function(code) {
|
||||||
|
if (me.onExit)
|
||||||
|
me.onExit(code);
|
||||||
|
me.rtenv = null;
|
||||||
|
}
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
preloader.preloadedFiles.forEach(function(file) {
|
||||||
|
me.rtenv['copyToFS'](file.path, file.buffer);
|
||||||
|
});
|
||||||
|
preloader.preloadedFiles.length = 0; // Clear memory
|
||||||
|
me.rtenv['callMain'](args);
|
||||||
|
initPromise = null;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Engine.prototype.startGame = function(execName, mainPack, extraArgs) {
|
||||||
|
// Start and init with execName as loadPath if not inited.
|
||||||
|
this.executableName = execName;
|
||||||
|
var me = this;
|
||||||
|
return Promise.all([
|
||||||
|
this.init(execName),
|
||||||
|
this.preloadFile(mainPack, mainPack)
|
||||||
|
]).then(function() {
|
||||||
|
var args = ['--main-pack', mainPack];
|
||||||
|
if (extraArgs)
|
||||||
|
args = args.concat(extraArgs);
|
||||||
|
return me.start.apply(me, args);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Engine.prototype.setWebAssemblyFilenameExtension = function(override) {
|
||||||
|
if (String(override).length === 0) {
|
||||||
|
throw new Error('Invalid WebAssembly filename extension override');
|
||||||
|
}
|
||||||
|
wasmExt = String(override);
|
||||||
|
};
|
||||||
|
|
||||||
|
Engine.prototype.setUnloadAfterInit = function(enabled) {
|
||||||
|
unloadAfterInit = enabled;
|
||||||
|
};
|
||||||
|
|
||||||
|
Engine.prototype.setCanvas = function(canvasElem) {
|
||||||
|
this.canvas = canvasElem;
|
||||||
|
};
|
||||||
|
|
||||||
|
Engine.prototype.setCanvasResizedOnStart = function(enabled) {
|
||||||
|
this.resizeCanvasOnStart = enabled;
|
||||||
|
};
|
||||||
|
|
||||||
|
Engine.prototype.setLocale = function(locale) {
|
||||||
|
this.customLocale = locale;
|
||||||
|
};
|
||||||
|
|
||||||
|
Engine.prototype.setExecutableName = function(newName) {
|
||||||
|
this.executableName = newName;
|
||||||
|
};
|
||||||
|
|
||||||
|
Engine.prototype.setProgressFunc = function(func) {
|
||||||
|
progressFunc = func;
|
||||||
|
};
|
||||||
|
|
||||||
|
Engine.prototype.setStdoutFunc = function(func) {
|
||||||
|
var print = function(text) {
|
||||||
|
if (arguments.length > 1) {
|
||||||
|
text = Array.prototype.slice.call(arguments).join(" ");
|
||||||
|
}
|
||||||
|
func(text);
|
||||||
|
};
|
||||||
|
if (this.rtenv)
|
||||||
|
this.rtenv.print = print;
|
||||||
|
stdout = print;
|
||||||
|
};
|
||||||
|
|
||||||
|
Engine.prototype.setStderrFunc = function(func) {
|
||||||
|
var printErr = function(text) {
|
||||||
|
if (arguments.length > 1)
|
||||||
|
text = Array.prototype.slice.call(arguments).join(" ");
|
||||||
|
func(text);
|
||||||
|
};
|
||||||
|
if (this.rtenv)
|
||||||
|
this.rtenv.printErr = printErr;
|
||||||
|
stderr = printErr;
|
||||||
|
};
|
||||||
|
|
||||||
|
Engine.prototype.setOnExecute = function(onExecute) {
|
||||||
|
if (this.rtenv)
|
||||||
|
this.rtenv.onExecute = onExecute;
|
||||||
|
this.onExecute = onExecute;
|
||||||
|
}
|
||||||
|
|
||||||
|
Engine.prototype.setOnExit = function(onExit) {
|
||||||
|
this.onExit = onExit;
|
||||||
|
}
|
||||||
|
|
||||||
|
Engine.prototype.copyToFS = function(path, buffer) {
|
||||||
|
if (this.rtenv == null) {
|
||||||
|
throw new Error("Engine must be inited before copying files");
|
||||||
|
}
|
||||||
|
this.rtenv['copyToFS'](path, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closure compiler exported engine methods.
|
||||||
|
/** @export */
|
||||||
|
Engine['isWebGLAvailable'] = Utils.isWebGLAvailable;
|
||||||
|
Engine['load'] = load;
|
||||||
|
Engine['unload'] = unload;
|
||||||
|
Engine.prototype['init'] = Engine.prototype.init;
|
||||||
|
Engine.prototype['preloadFile'] = Engine.prototype.preloadFile;
|
||||||
|
Engine.prototype['start'] = Engine.prototype.start;
|
||||||
|
Engine.prototype['startGame'] = Engine.prototype.startGame;
|
||||||
|
Engine.prototype['setWebAssemblyFilenameExtension'] = Engine.prototype.setWebAssemblyFilenameExtension;
|
||||||
|
Engine.prototype['setUnloadAfterInit'] = Engine.prototype.setUnloadAfterInit;
|
||||||
|
Engine.prototype['setCanvas'] = Engine.prototype.setCanvas;
|
||||||
|
Engine.prototype['setCanvasResizedOnStart'] = Engine.prototype.setCanvasResizedOnStart;
|
||||||
|
Engine.prototype['setLocale'] = Engine.prototype.setLocale;
|
||||||
|
Engine.prototype['setExecutableName'] = Engine.prototype.setExecutableName;
|
||||||
|
Engine.prototype['setProgressFunc'] = Engine.prototype.setProgressFunc;
|
||||||
|
Engine.prototype['setStdoutFunc'] = Engine.prototype.setStdoutFunc;
|
||||||
|
Engine.prototype['setStderrFunc'] = Engine.prototype.setStderrFunc;
|
||||||
|
Engine.prototype['setOnExecute'] = Engine.prototype.setOnExecute;
|
||||||
|
Engine.prototype['setOnExit'] = Engine.prototype.setOnExit;
|
||||||
|
Engine.prototype['copyToFS'] = Engine.prototype.copyToFS;
|
||||||
|
return Engine;
|
||||||
|
})();
|
|
@ -0,0 +1,3 @@
|
||||||
|
var Godot;
|
||||||
|
var WebAssembly = {};
|
||||||
|
WebAssembly.instantiate = function(buffer, imports) {};
|
|
@ -0,0 +1,139 @@
|
||||||
|
var Preloader = /** @constructor */ function() {
|
||||||
|
|
||||||
|
var DOWNLOAD_ATTEMPTS_MAX = 4;
|
||||||
|
var progressFunc = null;
|
||||||
|
var lastProgress = { loaded: 0, total: 0 };
|
||||||
|
|
||||||
|
var loadingFiles = {};
|
||||||
|
this.preloadedFiles = [];
|
||||||
|
|
||||||
|
function loadXHR(resolve, reject, file, tracker) {
|
||||||
|
var xhr = new XMLHttpRequest;
|
||||||
|
xhr.open('GET', file);
|
||||||
|
if (!file.endsWith('.js')) {
|
||||||
|
xhr.responseType = 'arraybuffer';
|
||||||
|
}
|
||||||
|
['loadstart', 'progress', 'load', 'error', 'abort'].forEach(function(ev) {
|
||||||
|
xhr.addEventListener(ev, onXHREvent.bind(xhr, resolve, reject, file, tracker));
|
||||||
|
});
|
||||||
|
xhr.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onXHREvent(resolve, reject, file, tracker, ev) {
|
||||||
|
|
||||||
|
if (this.status >= 400) {
|
||||||
|
|
||||||
|
if (this.status < 500 || ++tracker[file].attempts >= DOWNLOAD_ATTEMPTS_MAX) {
|
||||||
|
reject(new Error("Failed loading file '" + file + "': " + this.statusText));
|
||||||
|
this.abort();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
setTimeout(loadXHR.bind(null, resolve, reject, file, tracker), 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (ev.type) {
|
||||||
|
case 'loadstart':
|
||||||
|
if (tracker[file] === undefined) {
|
||||||
|
tracker[file] = {
|
||||||
|
total: ev.total,
|
||||||
|
loaded: ev.loaded,
|
||||||
|
attempts: 0,
|
||||||
|
final: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'progress':
|
||||||
|
tracker[file].loaded = ev.loaded;
|
||||||
|
tracker[file].total = ev.total;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'load':
|
||||||
|
tracker[file].final = true;
|
||||||
|
resolve(this);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'error':
|
||||||
|
if (++tracker[file].attempts >= DOWNLOAD_ATTEMPTS_MAX) {
|
||||||
|
tracker[file].final = true;
|
||||||
|
reject(new Error("Failed loading file '" + file + "'"));
|
||||||
|
} else {
|
||||||
|
setTimeout(loadXHR.bind(null, resolve, reject, file, tracker), 1000);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'abort':
|
||||||
|
tracker[file].final = true;
|
||||||
|
reject(new Error("Loading file '" + file + "' was aborted."));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadPromise = function(file) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
loadXHR(resolve, reject, file, loadingFiles);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.preload = function(pathOrBuffer, destPath) {
|
||||||
|
if (pathOrBuffer instanceof ArrayBuffer) {
|
||||||
|
pathOrBuffer = new Uint8Array(pathOrBuffer);
|
||||||
|
} else if (ArrayBuffer.isView(pathOrBuffer)) {
|
||||||
|
pathOrBuffer = new Uint8Array(pathOrBuffer.buffer);
|
||||||
|
}
|
||||||
|
if (pathOrBuffer instanceof Uint8Array) {
|
||||||
|
this.preloadedFiles.push({
|
||||||
|
path: destPath,
|
||||||
|
buffer: pathOrBuffer
|
||||||
|
});
|
||||||
|
return Promise.resolve();
|
||||||
|
} else if (typeof pathOrBuffer === 'string') {
|
||||||
|
var me = this;
|
||||||
|
return this.loadPromise(pathOrBuffer).then(function(xhr) {
|
||||||
|
me.preloadedFiles.push({
|
||||||
|
path: destPath || pathOrBuffer,
|
||||||
|
buffer: xhr.response
|
||||||
|
});
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw Promise.reject("Invalid object for preloading");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var animateProgress = function() {
|
||||||
|
|
||||||
|
var loaded = 0;
|
||||||
|
var total = 0;
|
||||||
|
var totalIsValid = true;
|
||||||
|
var progressIsFinal = true;
|
||||||
|
|
||||||
|
Object.keys(loadingFiles).forEach(function(file) {
|
||||||
|
const stat = loadingFiles[file];
|
||||||
|
if (!stat.final) {
|
||||||
|
progressIsFinal = false;
|
||||||
|
}
|
||||||
|
if (!totalIsValid || stat.total === 0) {
|
||||||
|
totalIsValid = false;
|
||||||
|
total = 0;
|
||||||
|
} else {
|
||||||
|
total += stat.total;
|
||||||
|
}
|
||||||
|
loaded += stat.loaded;
|
||||||
|
});
|
||||||
|
if (loaded !== lastProgress.loaded || total !== lastProgress.total) {
|
||||||
|
lastProgress.loaded = loaded;
|
||||||
|
lastProgress.total = total;
|
||||||
|
if (typeof progressFunc === 'function')
|
||||||
|
progressFunc(loaded, total);
|
||||||
|
}
|
||||||
|
if (!progressIsFinal)
|
||||||
|
requestAnimationFrame(animateProgress);
|
||||||
|
}
|
||||||
|
this.animateProgress = animateProgress; // Also exposed to start it.
|
||||||
|
|
||||||
|
this.setProgressFunc = function(callback) {
|
||||||
|
progressFunc = callback;
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,51 @@
|
||||||
|
var Utils = {
|
||||||
|
|
||||||
|
createLocateRewrite: function(execName) {
|
||||||
|
function rw(path) {
|
||||||
|
if (path.endsWith('.worker.js')) {
|
||||||
|
return execName + '.worker.js';
|
||||||
|
} else if (path.endsWith('.js')) {
|
||||||
|
return execName + '.js';
|
||||||
|
} else if (path.endsWith('.wasm')) {
|
||||||
|
return execName + '.wasm';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rw;
|
||||||
|
},
|
||||||
|
|
||||||
|
createInstantiatePromise: function(wasmLoader) {
|
||||||
|
function instantiateWasm(imports, onSuccess) {
|
||||||
|
wasmLoader.then(function(xhr) {
|
||||||
|
WebAssembly.instantiate(xhr.response, imports).then(function(result) {
|
||||||
|
onSuccess(result['instance'], result['module']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
wasmLoader = null;
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
return instantiateWasm;
|
||||||
|
},
|
||||||
|
|
||||||
|
findCanvas: function() {
|
||||||
|
var nodes = document.getElementsByTagName('canvas');
|
||||||
|
if (nodes.length && nodes[0] instanceof HTMLCanvasElement) {
|
||||||
|
return nodes[0];
|
||||||
|
}
|
||||||
|
throw new Error("No canvas found");
|
||||||
|
},
|
||||||
|
|
||||||
|
isWebGLAvailable: function(majorVersion = 1) {
|
||||||
|
|
||||||
|
var testContext = false;
|
||||||
|
try {
|
||||||
|
var testCanvas = document.createElement('canvas');
|
||||||
|
if (majorVersion === 1) {
|
||||||
|
testContext = testCanvas.getContext('webgl') || testCanvas.getContext('experimental-webgl');
|
||||||
|
} else if (majorVersion === 2) {
|
||||||
|
testContext = testCanvas.getContext('webgl2') || testCanvas.getContext('experimental-webgl2');
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
return !!testContext;
|
||||||
|
}
|
||||||
|
};
|
|
@ -94,6 +94,9 @@ public:
|
||||||
} else if (req[1] == basereq + ".js") {
|
} else if (req[1] == basereq + ".js") {
|
||||||
filepath += ".js";
|
filepath += ".js";
|
||||||
ctype = "application/javascript";
|
ctype = "application/javascript";
|
||||||
|
} else if (req[1] == basereq + ".worker.js") {
|
||||||
|
filepath += ".worker.js";
|
||||||
|
ctype = "application/javascript";
|
||||||
} else if (req[1] == basereq + ".pck") {
|
} else if (req[1] == basereq + ".pck") {
|
||||||
filepath += ".pck";
|
filepath += ".pck";
|
||||||
ctype = "application/octet-stream";
|
ctype = "application/octet-stream";
|
||||||
|
@ -435,6 +438,10 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese
|
||||||
} else if (file == "godot.js") {
|
} else if (file == "godot.js") {
|
||||||
|
|
||||||
file = p_path.get_file().get_basename() + ".js";
|
file = p_path.get_file().get_basename() + ".js";
|
||||||
|
} else if (file == "godot.worker.js") {
|
||||||
|
|
||||||
|
file = p_path.get_file().get_basename() + ".worker.js";
|
||||||
|
|
||||||
} else if (file == "godot.wasm") {
|
} else if (file == "godot.wasm") {
|
||||||
|
|
||||||
file = p_path.get_file().get_basename() + ".wasm";
|
file = p_path.get_file().get_basename() + ".wasm";
|
||||||
|
@ -568,6 +575,7 @@ Error EditorExportPlatformJavaScript::run(const Ref<EditorExportPreset> &p_prese
|
||||||
// Export generates several files, clean them up on failure.
|
// Export generates several files, clean them up on failure.
|
||||||
DirAccess::remove_file_or_error(basepath + ".html");
|
DirAccess::remove_file_or_error(basepath + ".html");
|
||||||
DirAccess::remove_file_or_error(basepath + ".js");
|
DirAccess::remove_file_or_error(basepath + ".js");
|
||||||
|
DirAccess::remove_file_or_error(basepath + ".worker.js");
|
||||||
DirAccess::remove_file_or_error(basepath + ".pck");
|
DirAccess::remove_file_or_error(basepath + ".pck");
|
||||||
DirAccess::remove_file_or_error(basepath + ".png");
|
DirAccess::remove_file_or_error(basepath + ".png");
|
||||||
DirAccess::remove_file_or_error(basepath + ".wasm");
|
DirAccess::remove_file_or_error(basepath + ".wasm");
|
||||||
|
|
|
@ -34,27 +34,98 @@
|
||||||
|
|
||||||
#include <emscripten/emscripten.h>
|
#include <emscripten/emscripten.h>
|
||||||
|
|
||||||
|
static OS_JavaScript *os = NULL;
|
||||||
|
|
||||||
|
// Files drop (implemented in JS for now).
|
||||||
|
extern "C" EMSCRIPTEN_KEEPALIVE void _drop_files_callback(char *p_filev[], int p_filec) {
|
||||||
|
if (!os || !os->get_main_loop()) {
|
||||||
|
ERR_FAIL_MSG("Unable to drop files because the OS or MainLoop are not active");
|
||||||
|
}
|
||||||
|
Vector<String> files;
|
||||||
|
for (int i = 0; i < p_filec; i++) {
|
||||||
|
files.push_back(String::utf8(p_filev[i]));
|
||||||
|
}
|
||||||
|
os->get_main_loop()->drop_files(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
void exit_callback() {
|
||||||
|
emscripten_cancel_main_loop(); // After this, we can exit!
|
||||||
|
Main::cleanup();
|
||||||
|
int exit_code = OS_JavaScript::get_singleton()->get_exit_code();
|
||||||
|
memdelete(os);
|
||||||
|
os = NULL;
|
||||||
|
emscripten_force_exit(exit_code); // No matter that we call cancel_main_loop, regular "exit" will not work, forcing.
|
||||||
|
}
|
||||||
|
|
||||||
|
void main_loop_callback() {
|
||||||
|
|
||||||
|
if (os->main_loop_iterate()) {
|
||||||
|
emscripten_cancel_main_loop(); // Cancel current loop and wait for finalize_async.
|
||||||
|
EM_ASM({
|
||||||
|
// This will contain the list of operations that need to complete before cleanup.
|
||||||
|
Module.async_finish = [];
|
||||||
|
});
|
||||||
|
os->get_main_loop()->finish();
|
||||||
|
os->finalize_async(); // Will add all the async finish functions.
|
||||||
|
EM_ASM({
|
||||||
|
Promise.all(Module.async_finish).then(function() {
|
||||||
|
Module.async_finish = [];
|
||||||
|
ccall("cleanup_after_sync", null, []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" EMSCRIPTEN_KEEPALIVE void cleanup_after_sync() {
|
||||||
|
emscripten_set_main_loop(exit_callback, -1, false);
|
||||||
|
}
|
||||||
|
|
||||||
extern "C" EMSCRIPTEN_KEEPALIVE void main_after_fs_sync(char *p_idbfs_err) {
|
extern "C" EMSCRIPTEN_KEEPALIVE void main_after_fs_sync(char *p_idbfs_err) {
|
||||||
|
|
||||||
|
OS_JavaScript *os = OS_JavaScript::get_singleton();
|
||||||
|
|
||||||
|
// Set IDBFS status
|
||||||
String idbfs_err = String::utf8(p_idbfs_err);
|
String idbfs_err = String::utf8(p_idbfs_err);
|
||||||
if (!idbfs_err.empty()) {
|
if (!idbfs_err.empty()) {
|
||||||
print_line("IndexedDB not available: " + idbfs_err);
|
print_line("IndexedDB not available: " + idbfs_err);
|
||||||
}
|
}
|
||||||
OS_JavaScript *os = OS_JavaScript::get_singleton();
|
|
||||||
os->set_idb_available(idbfs_err.empty());
|
os->set_idb_available(idbfs_err.empty());
|
||||||
|
|
||||||
|
// Set canvas ID
|
||||||
|
char canvas_ptr[256];
|
||||||
|
/* clang-format off */
|
||||||
|
EM_ASM({
|
||||||
|
stringToUTF8("#" + Module['canvas'].id, $0, 255);
|
||||||
|
}, canvas_ptr);
|
||||||
|
/* clang-format on */
|
||||||
|
os->canvas_id.parse_utf8(canvas_ptr, 255);
|
||||||
|
|
||||||
|
// Set locale
|
||||||
|
char locale_ptr[16];
|
||||||
|
/* clang-format off */
|
||||||
|
EM_ASM({
|
||||||
|
stringToUTF8(Module['locale'], $0, 16);
|
||||||
|
}, locale_ptr);
|
||||||
|
|
||||||
|
/* clang-format on */
|
||||||
|
setenv("LANG", locale_ptr, true);
|
||||||
|
|
||||||
Main::setup2();
|
Main::setup2();
|
||||||
// Ease up compatibility.
|
// Ease up compatibility.
|
||||||
ResourceLoader::set_abort_on_missing_resources(false);
|
ResourceLoader::set_abort_on_missing_resources(false);
|
||||||
Main::start();
|
Main::start();
|
||||||
os->run_async();
|
os->get_main_loop()->init();
|
||||||
os->main_loop_iterate();
|
main_loop_callback();
|
||||||
|
emscripten_resume_main_loop();
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char *argv[]) {
|
||||||
|
|
||||||
new OS_JavaScript(argc, argv);
|
os = new OS_JavaScript(argc, argv);
|
||||||
// TODO: Check error return value.
|
// TODO: Check error return value.
|
||||||
Main::setup(argv[0], argc - 1, &argv[1], false);
|
Main::setup(argv[0], argc - 1, &argv[1], false);
|
||||||
|
emscripten_set_main_loop(main_loop_callback, -1, false);
|
||||||
|
emscripten_pause_main_loop(); // Will need to wait for FS sync.
|
||||||
|
|
||||||
// Sync from persistent state into memory and then
|
// Sync from persistent state into memory and then
|
||||||
// run the 'main_after_fs_sync' function.
|
// run the 'main_after_fs_sync' function.
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||||
/*************************************************************************/
|
/*************************************************************************/
|
||||||
|
|
||||||
var IDHandler = function() {
|
var IDHandler = /** @constructor */ function() {
|
||||||
|
|
||||||
var ids = {};
|
var ids = {};
|
||||||
var size = 0;
|
var size = 0;
|
|
@ -0,0 +1,204 @@
|
||||||
|
/*************************************************************************/
|
||||||
|
/* utils.js */
|
||||||
|
/*************************************************************************/
|
||||||
|
/* This file is part of: */
|
||||||
|
/* GODOT ENGINE */
|
||||||
|
/* https://godotengine.org */
|
||||||
|
/*************************************************************************/
|
||||||
|
/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
|
||||||
|
/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
|
||||||
|
/* */
|
||||||
|
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||||
|
/* a copy of this software and associated documentation files (the */
|
||||||
|
/* "Software"), to deal in the Software without restriction, including */
|
||||||
|
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||||
|
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||||
|
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||||
|
/* the following conditions: */
|
||||||
|
/* */
|
||||||
|
/* The above copyright notice and this permission notice shall be */
|
||||||
|
/* included in all copies or substantial portions of the Software. */
|
||||||
|
/* */
|
||||||
|
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||||
|
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||||
|
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
|
||||||
|
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||||
|
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||||
|
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||||
|
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||||
|
/*************************************************************************/
|
||||||
|
|
||||||
|
Module['copyToFS'] = function(path, buffer) {
|
||||||
|
var p = path.lastIndexOf("/");
|
||||||
|
var dir = "/";
|
||||||
|
if (p > 0) {
|
||||||
|
dir = path.slice(0, path.lastIndexOf("/"));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
FS.stat(dir);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.errno !== ERRNO_CODES.ENOENT) { // 'ENOENT', see https://github.com/emscripten-core/emscripten/blob/master/system/lib/libc/musl/arch/emscripten/bits/errno.h
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
FS.mkdirTree(dir);
|
||||||
|
}
|
||||||
|
// With memory growth, canOwn should be false.
|
||||||
|
FS.writeFile(path, new Uint8Array(buffer), {'flags': 'wx+'});
|
||||||
|
}
|
||||||
|
|
||||||
|
Module.drop_handler = (function() {
|
||||||
|
var upload = [];
|
||||||
|
var uploadPromises = [];
|
||||||
|
var uploadCallback = null;
|
||||||
|
|
||||||
|
function readFilePromise(entry, path) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
entry.file(function(file) {
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.onload = function() {
|
||||||
|
var f = {
|
||||||
|
"path": file.relativePath || file.webkitRelativePath,
|
||||||
|
"name": file.name,
|
||||||
|
"type": file.type,
|
||||||
|
"size": file.size,
|
||||||
|
"data": reader.result
|
||||||
|
};
|
||||||
|
if (!f['path'])
|
||||||
|
f['path'] = f['name'];
|
||||||
|
upload.push(f);
|
||||||
|
resolve()
|
||||||
|
};
|
||||||
|
reader.onerror = function() {
|
||||||
|
console.log("Error reading file");
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
|
||||||
|
}, function(err) {
|
||||||
|
console.log("Error!");
|
||||||
|
reject();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function readDirectoryPromise(entry) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
var reader = entry.createReader();
|
||||||
|
reader.readEntries(function(entries) {
|
||||||
|
for (var i = 0; i < entries.length; i++) {
|
||||||
|
var ent = entries[i];
|
||||||
|
if (ent.isDirectory) {
|
||||||
|
uploadPromises.push(readDirectoryPromise(ent));
|
||||||
|
} else if (ent.isFile) {
|
||||||
|
uploadPromises.push(readFilePromise(ent));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function processUploadsPromises(resolve, reject) {
|
||||||
|
if (uploadPromises.length == 0) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uploadPromises.pop().then(function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
processUploadsPromises(resolve, reject);
|
||||||
|
//processUploadsPromises.bind(null, resolve, reject)
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function dropFiles(files) {
|
||||||
|
var args = files || [];
|
||||||
|
var argc = args.length;
|
||||||
|
var argv = stackAlloc((argc + 1) * 4);
|
||||||
|
for (var i = 0; i < argc; i++) {
|
||||||
|
HEAP32[(argv >> 2) + i] = allocateUTF8OnStack(args[i]);
|
||||||
|
}
|
||||||
|
HEAP32[(argv >> 2) + argc] = 0;
|
||||||
|
// Defined in javascript_main.cpp
|
||||||
|
ccall('_drop_files_callback', 'void', ['number', 'number'], [argv, argc]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return function(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
if (ev.dataTransfer.items) {
|
||||||
|
// Use DataTransferItemList interface to access the file(s)
|
||||||
|
for (var i = 0; i < ev.dataTransfer.items.length; i++) {
|
||||||
|
const item = ev.dataTransfer.items[i];
|
||||||
|
var entry = null;
|
||||||
|
if ("getAsEntry" in item) {
|
||||||
|
entry = item.getAsEntry();
|
||||||
|
} else if ("webkitGetAsEntry" in item) {
|
||||||
|
entry = item.webkitGetAsEntry();
|
||||||
|
}
|
||||||
|
if (!entry) {
|
||||||
|
console.error("File upload not supported");
|
||||||
|
} else if (entry.isDirectory) {
|
||||||
|
uploadPromises.push(readDirectoryPromise(entry));
|
||||||
|
} else if (entry.isFile) {
|
||||||
|
uploadPromises.push(readFilePromise(entry));
|
||||||
|
} else {
|
||||||
|
console.error("Unrecognized entry...", entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("File upload not supported");
|
||||||
|
}
|
||||||
|
uploadCallback = new Promise(processUploadsPromises).then(function() {
|
||||||
|
const DROP = "/tmp/drop-" + parseInt(Math.random() * Math.pow(2, 31)) + "/";
|
||||||
|
var drops = [];
|
||||||
|
var files = [];
|
||||||
|
upload.forEach((elem) => {
|
||||||
|
var path = elem['path'];
|
||||||
|
Module['copyToFS'](DROP + path, elem['data']);
|
||||||
|
var idx = path.indexOf("/");
|
||||||
|
if (idx == -1) {
|
||||||
|
// Root file
|
||||||
|
drops.push(DROP + path);
|
||||||
|
} else {
|
||||||
|
// Subdir
|
||||||
|
var sub = path.substr(0, idx);
|
||||||
|
idx = sub.indexOf("/");
|
||||||
|
if (idx < 0 && drops.indexOf(DROP + sub) == -1) {
|
||||||
|
drops.push(DROP + sub);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
files.push(DROP + path);
|
||||||
|
});
|
||||||
|
uploadPromises = [];
|
||||||
|
upload = [];
|
||||||
|
dropFiles(drops);
|
||||||
|
var dirs = [DROP.substr(0, DROP.length -1)];
|
||||||
|
files.forEach(function (file) {
|
||||||
|
FS.unlink(file);
|
||||||
|
var dir = file.replace(DROP, "");
|
||||||
|
var idx = dir.lastIndexOf("/");
|
||||||
|
while (idx > 0) {
|
||||||
|
dir = dir.substr(0, idx);
|
||||||
|
if (dirs.indexOf(DROP + dir) == -1) {
|
||||||
|
dirs.push(DROP + dir);
|
||||||
|
}
|
||||||
|
idx = dir.lastIndexOf("/");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Remove dirs.
|
||||||
|
dirs = dirs.sort(function(a, b) {
|
||||||
|
var al = (a.match(/\//g) || []).length;
|
||||||
|
var bl = (b.match(/\//g) || []).length;
|
||||||
|
if (al > bl)
|
||||||
|
return -1;
|
||||||
|
else if (al < bl)
|
||||||
|
return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
dirs.forEach(function(dir) {
|
||||||
|
FS.rmdir(dir);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
|
@ -31,12 +31,16 @@
|
||||||
#include "os_javascript.h"
|
#include "os_javascript.h"
|
||||||
|
|
||||||
#include "core/io/file_access_buffered_fa.h"
|
#include "core/io/file_access_buffered_fa.h"
|
||||||
|
#include "core/io/json.h"
|
||||||
#include "drivers/gles2/rasterizer_gles2.h"
|
#include "drivers/gles2/rasterizer_gles2.h"
|
||||||
#include "drivers/gles3/rasterizer_gles3.h"
|
#include "drivers/gles3/rasterizer_gles3.h"
|
||||||
#include "drivers/unix/dir_access_unix.h"
|
#include "drivers/unix/dir_access_unix.h"
|
||||||
#include "drivers/unix/file_access_unix.h"
|
#include "drivers/unix/file_access_unix.h"
|
||||||
#include "main/main.h"
|
#include "main/main.h"
|
||||||
#include "servers/visual/visual_server_raster.h"
|
#include "servers/visual/visual_server_raster.h"
|
||||||
|
#ifndef NO_THREADS
|
||||||
|
#include "servers/visual/visual_server_wrap_mt.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#include <emscripten.h>
|
#include <emscripten.h>
|
||||||
#include <png.h>
|
#include <png.h>
|
||||||
|
@ -49,42 +53,42 @@
|
||||||
#define DOM_BUTTON_RIGHT 2
|
#define DOM_BUTTON_RIGHT 2
|
||||||
#define DOM_BUTTON_XBUTTON1 3
|
#define DOM_BUTTON_XBUTTON1 3
|
||||||
#define DOM_BUTTON_XBUTTON2 4
|
#define DOM_BUTTON_XBUTTON2 4
|
||||||
#define GODOT_CANVAS_SELECTOR "#canvas"
|
|
||||||
|
|
||||||
// Window (canvas)
|
// Window (canvas)
|
||||||
|
|
||||||
static void focus_canvas() {
|
static void focus_canvas() {
|
||||||
|
|
||||||
/* clang-format off */
|
/* clang-format off */
|
||||||
EM_ASM(
|
EM_ASM({
|
||||||
Module.canvas.focus();
|
Module['canvas'].focus();
|
||||||
);
|
});
|
||||||
/* clang-format on */
|
/* clang-format on */
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool is_canvas_focused() {
|
static bool is_canvas_focused() {
|
||||||
|
|
||||||
/* clang-format off */
|
/* clang-format off */
|
||||||
return EM_ASM_INT_V(
|
return EM_ASM_INT({
|
||||||
return document.activeElement == Module.canvas;
|
return document.activeElement == Module['canvas'];
|
||||||
);
|
});
|
||||||
/* clang-format on */
|
/* clang-format on */
|
||||||
}
|
}
|
||||||
|
|
||||||
static Point2 compute_position_in_canvas(int x, int y) {
|
static Point2 compute_position_in_canvas(int x, int y) {
|
||||||
|
OS_JavaScript *os = OS_JavaScript::get_singleton();
|
||||||
int canvas_x = EM_ASM_INT({
|
int canvas_x = EM_ASM_INT({
|
||||||
return document.getElementById('canvas').getBoundingClientRect().x;
|
return Module['canvas'].getBoundingClientRect().x;
|
||||||
});
|
});
|
||||||
int canvas_y = EM_ASM_INT({
|
int canvas_y = EM_ASM_INT({
|
||||||
return document.getElementById('canvas').getBoundingClientRect().y;
|
return Module['canvas'].getBoundingClientRect().y;
|
||||||
});
|
});
|
||||||
int canvas_width;
|
int canvas_width;
|
||||||
int canvas_height;
|
int canvas_height;
|
||||||
emscripten_get_canvas_element_size(GODOT_CANVAS_SELECTOR, &canvas_width, &canvas_height);
|
emscripten_get_canvas_element_size(os->canvas_id.utf8().get_data(), &canvas_width, &canvas_height);
|
||||||
|
|
||||||
double element_width;
|
double element_width;
|
||||||
double element_height;
|
double element_height;
|
||||||
emscripten_get_element_css_size(GODOT_CANVAS_SELECTOR, &element_width, &element_height);
|
emscripten_get_element_css_size(os->canvas_id.utf8().get_data(), &element_width, &element_height);
|
||||||
|
|
||||||
return Point2((int)(canvas_width / element_width * (x - canvas_x)),
|
return Point2((int)(canvas_width / element_width * (x - canvas_x)),
|
||||||
(int)(canvas_height / element_height * (y - canvas_y)));
|
(int)(canvas_height / element_height * (y - canvas_y)));
|
||||||
|
@ -92,6 +96,16 @@ static Point2 compute_position_in_canvas(int x, int y) {
|
||||||
|
|
||||||
static bool cursor_inside_canvas = true;
|
static bool cursor_inside_canvas = true;
|
||||||
|
|
||||||
|
extern "C" EMSCRIPTEN_KEEPALIVE void _canvas_resize_callback() {
|
||||||
|
OS_JavaScript *os = OS_JavaScript::get_singleton();
|
||||||
|
int canvas_width;
|
||||||
|
int canvas_height;
|
||||||
|
// Update the framebuffer size.
|
||||||
|
emscripten_get_canvas_element_size(os->canvas_id.utf8().get_data(), &canvas_width, &canvas_height);
|
||||||
|
emscripten_set_canvas_element_size(os->canvas_id.utf8().get_data(), canvas_width, canvas_height);
|
||||||
|
Main::force_redraw();
|
||||||
|
}
|
||||||
|
|
||||||
EM_BOOL OS_JavaScript::fullscreen_change_callback(int p_event_type, const EmscriptenFullscreenChangeEvent *p_event, void *p_user_data) {
|
EM_BOOL OS_JavaScript::fullscreen_change_callback(int p_event_type, const EmscriptenFullscreenChangeEvent *p_event, void *p_user_data) {
|
||||||
|
|
||||||
OS_JavaScript *os = get_singleton();
|
OS_JavaScript *os = get_singleton();
|
||||||
|
@ -141,14 +155,14 @@ void OS_JavaScript::set_window_size(const Size2 p_size) {
|
||||||
emscripten_exit_soft_fullscreen();
|
emscripten_exit_soft_fullscreen();
|
||||||
window_maximized = false;
|
window_maximized = false;
|
||||||
}
|
}
|
||||||
emscripten_set_canvas_element_size(GODOT_CANVAS_SELECTOR, p_size.x, p_size.y);
|
emscripten_set_canvas_element_size(canvas_id.utf8().get_data(), p_size.x, p_size.y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Size2 OS_JavaScript::get_window_size() const {
|
Size2 OS_JavaScript::get_window_size() const {
|
||||||
|
|
||||||
int canvas[2];
|
int canvas[2];
|
||||||
emscripten_get_canvas_element_size(GODOT_CANVAS_SELECTOR, canvas, canvas + 1);
|
emscripten_get_canvas_element_size(canvas_id.utf8().get_data(), canvas, canvas + 1);
|
||||||
return Size2(canvas[0], canvas[1]);
|
return Size2(canvas[0], canvas[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,7 +182,7 @@ void OS_JavaScript::set_window_maximized(bool p_enabled) {
|
||||||
strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF;
|
strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF;
|
||||||
strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT;
|
strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT;
|
||||||
strategy.canvasResizedCallback = NULL;
|
strategy.canvasResizedCallback = NULL;
|
||||||
emscripten_enter_soft_fullscreen(GODOT_CANVAS_SELECTOR, &strategy);
|
emscripten_enter_soft_fullscreen(canvas_id.utf8().get_data(), &strategy);
|
||||||
window_maximized = p_enabled;
|
window_maximized = p_enabled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -197,7 +211,7 @@ void OS_JavaScript::set_window_fullscreen(bool p_enabled) {
|
||||||
strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF;
|
strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF;
|
||||||
strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT;
|
strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT;
|
||||||
strategy.canvasResizedCallback = NULL;
|
strategy.canvasResizedCallback = NULL;
|
||||||
EMSCRIPTEN_RESULT result = emscripten_request_fullscreen_strategy(GODOT_CANVAS_SELECTOR, false, &strategy);
|
EMSCRIPTEN_RESULT result = emscripten_request_fullscreen_strategy(canvas_id.utf8().get_data(), false, &strategy);
|
||||||
ERR_FAIL_COND_MSG(result == EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED, "Enabling fullscreen is only possible from an input callback for the HTML5 platform.");
|
ERR_FAIL_COND_MSG(result == EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED, "Enabling fullscreen is only possible from an input callback for the HTML5 platform.");
|
||||||
ERR_FAIL_COND_MSG(result != EMSCRIPTEN_RESULT_SUCCESS, "Enabling fullscreen is only possible from an input callback for the HTML5 platform.");
|
ERR_FAIL_COND_MSG(result != EMSCRIPTEN_RESULT_SUCCESS, "Enabling fullscreen is only possible from an input callback for the HTML5 platform.");
|
||||||
// Not fullscreen yet, so prevent "windowed" canvas dimensions from
|
// Not fullscreen yet, so prevent "windowed" canvas dimensions from
|
||||||
|
@ -434,8 +448,8 @@ static const char *godot2dom_cursor(OS::CursorShape p_shape) {
|
||||||
static void set_css_cursor(const char *p_cursor) {
|
static void set_css_cursor(const char *p_cursor) {
|
||||||
|
|
||||||
/* clang-format off */
|
/* clang-format off */
|
||||||
EM_ASM_({
|
EM_ASM({
|
||||||
Module.canvas.style.cursor = UTF8ToString($0);
|
Module['canvas'].style.cursor = UTF8ToString($0);
|
||||||
}, p_cursor);
|
}, p_cursor);
|
||||||
/* clang-format on */
|
/* clang-format on */
|
||||||
}
|
}
|
||||||
|
@ -444,7 +458,7 @@ static bool is_css_cursor_hidden() {
|
||||||
|
|
||||||
/* clang-format off */
|
/* clang-format off */
|
||||||
return EM_ASM_INT({
|
return EM_ASM_INT({
|
||||||
return Module.canvas.style.cursor === 'none';
|
return Module['canvas'].style.cursor === 'none';
|
||||||
});
|
});
|
||||||
/* clang-format on */
|
/* clang-format on */
|
||||||
}
|
}
|
||||||
|
@ -697,9 +711,9 @@ EM_BOOL OS_JavaScript::wheel_callback(int p_event_type, const EmscriptenWheelEve
|
||||||
bool OS_JavaScript::has_touchscreen_ui_hint() const {
|
bool OS_JavaScript::has_touchscreen_ui_hint() const {
|
||||||
|
|
||||||
/* clang-format off */
|
/* clang-format off */
|
||||||
return EM_ASM_INT_V(
|
return EM_ASM_INT({
|
||||||
return 'ontouchstart' in window;
|
return 'ontouchstart' in window;
|
||||||
);
|
});
|
||||||
/* clang-format on */
|
/* clang-format on */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -902,6 +916,7 @@ Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver,
|
||||||
emscripten_webgl_init_context_attributes(&attributes);
|
emscripten_webgl_init_context_attributes(&attributes);
|
||||||
attributes.alpha = GLOBAL_GET("display/window/per_pixel_transparency/allowed");
|
attributes.alpha = GLOBAL_GET("display/window/per_pixel_transparency/allowed");
|
||||||
attributes.antialias = false;
|
attributes.antialias = false;
|
||||||
|
attributes.explicitSwapControl = true;
|
||||||
ERR_FAIL_INDEX_V(p_video_driver, VIDEO_DRIVER_MAX, ERR_INVALID_PARAMETER);
|
ERR_FAIL_INDEX_V(p_video_driver, VIDEO_DRIVER_MAX, ERR_INVALID_PARAMETER);
|
||||||
|
|
||||||
if (p_desired.layered) {
|
if (p_desired.layered) {
|
||||||
|
@ -945,8 +960,8 @@ Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = emscripten_webgl_create_context(GODOT_CANVAS_SELECTOR, &attributes);
|
webgl_ctx = emscripten_webgl_create_context(canvas_id.utf8().get_data(), &attributes);
|
||||||
if (emscripten_webgl_make_context_current(ctx) != EMSCRIPTEN_RESULT_SUCCESS) {
|
if (emscripten_webgl_make_context_current(webgl_ctx) != EMSCRIPTEN_RESULT_SUCCESS) {
|
||||||
gl_initialization_error = true;
|
gl_initialization_error = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -968,6 +983,7 @@ Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver,
|
||||||
if (p_desired.fullscreen) {
|
if (p_desired.fullscreen) {
|
||||||
/* clang-format off */
|
/* clang-format off */
|
||||||
EM_ASM({
|
EM_ASM({
|
||||||
|
const canvas = Module['canvas'];
|
||||||
(canvas.requestFullscreen || canvas.msRequestFullscreen ||
|
(canvas.requestFullscreen || canvas.msRequestFullscreen ||
|
||||||
canvas.mozRequestFullScreen || canvas.mozRequestFullscreen ||
|
canvas.mozRequestFullScreen || canvas.mozRequestFullscreen ||
|
||||||
canvas.webkitRequestFullscreen
|
canvas.webkitRequestFullscreen
|
||||||
|
@ -976,26 +992,22 @@ Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver,
|
||||||
/* clang-format on */
|
/* clang-format on */
|
||||||
}
|
}
|
||||||
/* clang-format off */
|
/* clang-format off */
|
||||||
if (EM_ASM_INT_V({ return Module.resizeCanvasOnStart })) {
|
if (EM_ASM_INT({ return Module['resizeCanvasOnStart'] })) {
|
||||||
/* clang-format on */
|
/* clang-format on */
|
||||||
set_window_size(Size2(video_mode.width, video_mode.height));
|
set_window_size(Size2(video_mode.width, video_mode.height));
|
||||||
} else {
|
} else {
|
||||||
set_window_size(get_window_size());
|
set_window_size(get_window_size());
|
||||||
}
|
}
|
||||||
|
|
||||||
char locale_ptr[16];
|
|
||||||
/* clang-format off */
|
|
||||||
EM_ASM_ARGS({
|
|
||||||
stringToUTF8(Module.locale, $0, 16);
|
|
||||||
}, locale_ptr);
|
|
||||||
/* clang-format on */
|
|
||||||
setenv("LANG", locale_ptr, true);
|
|
||||||
|
|
||||||
AudioDriverManager::initialize(p_audio_driver);
|
AudioDriverManager::initialize(p_audio_driver);
|
||||||
VisualServer *visual_server = memnew(VisualServerRaster());
|
visual_server = memnew(VisualServerRaster());
|
||||||
|
#ifndef NO_THREADS
|
||||||
|
visual_server = memnew(VisualServerWrapMT(visual_server, false));
|
||||||
|
#endif
|
||||||
input = memnew(InputDefault);
|
input = memnew(InputDefault);
|
||||||
|
|
||||||
EMSCRIPTEN_RESULT result;
|
EMSCRIPTEN_RESULT result;
|
||||||
|
CharString id = canvas_id.utf8().get_data();
|
||||||
#define EM_CHECK(ev) \
|
#define EM_CHECK(ev) \
|
||||||
if (result != EMSCRIPTEN_RESULT_SUCCESS) \
|
if (result != EMSCRIPTEN_RESULT_SUCCESS) \
|
||||||
ERR_PRINTS("Error while setting " #ev " callback: Code " + itos(result))
|
ERR_PRINTS("Error while setting " #ev " callback: Code " + itos(result))
|
||||||
|
@ -1009,16 +1021,16 @@ Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver,
|
||||||
// JavaScript APIs. For APIs that are not (sufficiently) exposed, EM_ASM
|
// JavaScript APIs. For APIs that are not (sufficiently) exposed, EM_ASM
|
||||||
// is used below.
|
// is used below.
|
||||||
SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, mousemove, mousemove_callback)
|
SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, mousemove, mousemove_callback)
|
||||||
SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, mousedown, mouse_button_callback)
|
SET_EM_CALLBACK(id.get_data(), mousedown, mouse_button_callback)
|
||||||
SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, mouseup, mouse_button_callback)
|
SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, mouseup, mouse_button_callback)
|
||||||
SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, wheel, wheel_callback)
|
SET_EM_CALLBACK(id.get_data(), wheel, wheel_callback)
|
||||||
SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, touchstart, touch_press_callback)
|
SET_EM_CALLBACK(id.get_data(), touchstart, touch_press_callback)
|
||||||
SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, touchmove, touchmove_callback)
|
SET_EM_CALLBACK(id.get_data(), touchmove, touchmove_callback)
|
||||||
SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, touchend, touch_press_callback)
|
SET_EM_CALLBACK(id.get_data(), touchend, touch_press_callback)
|
||||||
SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, touchcancel, touch_press_callback)
|
SET_EM_CALLBACK(id.get_data(), touchcancel, touch_press_callback)
|
||||||
SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, keydown, keydown_callback)
|
SET_EM_CALLBACK(id.get_data(), keydown, keydown_callback)
|
||||||
SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, keypress, keypress_callback)
|
SET_EM_CALLBACK(id.get_data(), keypress, keypress_callback)
|
||||||
SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, keyup, keyup_callback)
|
SET_EM_CALLBACK(id.get_data(), keyup, keyup_callback)
|
||||||
SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, fullscreenchange, fullscreen_change_callback)
|
SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, fullscreenchange, fullscreen_change_callback)
|
||||||
SET_EM_CALLBACK_NOTARGET(gamepadconnected, gamepad_change_callback)
|
SET_EM_CALLBACK_NOTARGET(gamepadconnected, gamepad_change_callback)
|
||||||
SET_EM_CALLBACK_NOTARGET(gamepaddisconnected, gamepad_change_callback)
|
SET_EM_CALLBACK_NOTARGET(gamepaddisconnected, gamepad_change_callback)
|
||||||
|
@ -1027,22 +1039,45 @@ Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver,
|
||||||
#undef EM_CHECK
|
#undef EM_CHECK
|
||||||
|
|
||||||
/* clang-format off */
|
/* clang-format off */
|
||||||
EM_ASM_ARGS({
|
EM_ASM({
|
||||||
|
Module.listeners = {};
|
||||||
|
const canvas = Module['canvas'];
|
||||||
const send_notification = cwrap('send_notification', null, ['number']);
|
const send_notification = cwrap('send_notification', null, ['number']);
|
||||||
const notifications = arguments;
|
const notifications = arguments;
|
||||||
(['mouseover', 'mouseleave', 'focus', 'blur']).forEach(function(event, index) {
|
(['mouseover', 'mouseleave', 'focus', 'blur']).forEach(function(event, index) {
|
||||||
Module.canvas.addEventListener(event, send_notification.bind(null, notifications[index]));
|
Module.listeners[event] = send_notification.bind(null, notifications[index]);
|
||||||
|
canvas.addEventListener(event, Module.listeners[event]);
|
||||||
});
|
});
|
||||||
// Clipboard
|
// Clipboard
|
||||||
const update_clipboard = cwrap('update_clipboard', null, ['string']);
|
const update_clipboard = cwrap('update_clipboard', null, ['string']);
|
||||||
window.addEventListener('paste', function(evt) {
|
Module.listeners['paste'] = function(evt) {
|
||||||
update_clipboard(evt.clipboardData.getData('text'));
|
update_clipboard(evt.clipboardData.getData('text'));
|
||||||
}, true);
|
};
|
||||||
|
window.addEventListener('paste', Module.listeners['paste'], true);
|
||||||
|
Module.listeners['dragover'] = function(ev) {
|
||||||
|
// Prevent default behavior (which would try to open the file(s))
|
||||||
|
ev.preventDefault();
|
||||||
|
};
|
||||||
|
// Drag an drop
|
||||||
|
Module.listeners['drop'] = Module.drop_handler; // Defined in native/utils.js
|
||||||
|
canvas.addEventListener('dragover', Module.listeners['dragover'], false);
|
||||||
|
canvas.addEventListener('drop', Module.listeners['drop'], false);
|
||||||
|
// Resize
|
||||||
|
const resize_callback = cwrap('_canvas_resize_callback', null, []);
|
||||||
|
Module.resize_observer = new window['ResizeObserver'](function(elements) {
|
||||||
|
resize_callback();
|
||||||
|
});
|
||||||
|
Module.resize_observer.observe(canvas);
|
||||||
|
// Quit request
|
||||||
|
Module['request_quit'] = function() {
|
||||||
|
send_notification(notifications[notifications.length - 1]);
|
||||||
|
};
|
||||||
},
|
},
|
||||||
MainLoop::NOTIFICATION_WM_MOUSE_ENTER,
|
MainLoop::NOTIFICATION_WM_MOUSE_ENTER,
|
||||||
MainLoop::NOTIFICATION_WM_MOUSE_EXIT,
|
MainLoop::NOTIFICATION_WM_MOUSE_EXIT,
|
||||||
MainLoop::NOTIFICATION_WM_FOCUS_IN,
|
MainLoop::NOTIFICATION_WM_FOCUS_IN,
|
||||||
MainLoop::NOTIFICATION_WM_FOCUS_OUT
|
MainLoop::NOTIFICATION_WM_FOCUS_OUT,
|
||||||
|
MainLoop::NOTIFICATION_WM_QUIT_REQUEST
|
||||||
);
|
);
|
||||||
/* clang-format on */
|
/* clang-format on */
|
||||||
|
|
||||||
|
@ -1051,6 +1086,10 @@ Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver,
|
||||||
return OK;
|
return OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OS_JavaScript::swap_buffers() {
|
||||||
|
emscripten_webgl_commit_frame();
|
||||||
|
}
|
||||||
|
|
||||||
void OS_JavaScript::set_main_loop(MainLoop *p_main_loop) {
|
void OS_JavaScript::set_main_loop(MainLoop *p_main_loop) {
|
||||||
|
|
||||||
main_loop = p_main_loop;
|
main_loop = p_main_loop;
|
||||||
|
@ -1062,17 +1101,6 @@ MainLoop *OS_JavaScript::get_main_loop() const {
|
||||||
return main_loop;
|
return main_loop;
|
||||||
}
|
}
|
||||||
|
|
||||||
void OS_JavaScript::run_async() {
|
|
||||||
|
|
||||||
main_loop->init();
|
|
||||||
emscripten_set_main_loop(main_loop_callback, -1, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void OS_JavaScript::main_loop_callback() {
|
|
||||||
|
|
||||||
get_singleton()->main_loop_iterate();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OS_JavaScript::main_loop_iterate() {
|
bool OS_JavaScript::main_loop_iterate() {
|
||||||
|
|
||||||
if (is_userfs_persistent() && sync_wait_time >= 0) {
|
if (is_userfs_persistent() && sync_wait_time >= 0) {
|
||||||
|
@ -1103,15 +1131,16 @@ bool OS_JavaScript::main_loop_iterate() {
|
||||||
strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF;
|
strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF;
|
||||||
strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT;
|
strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT;
|
||||||
strategy.canvasResizedCallback = NULL;
|
strategy.canvasResizedCallback = NULL;
|
||||||
emscripten_enter_soft_fullscreen(GODOT_CANVAS_SELECTOR, &strategy);
|
emscripten_enter_soft_fullscreen(canvas_id.utf8().get_data(), &strategy);
|
||||||
} else {
|
} else {
|
||||||
emscripten_set_canvas_element_size(GODOT_CANVAS_SELECTOR, windowed_size.width, windowed_size.height);
|
emscripten_set_canvas_element_size(canvas_id.utf8().get_data(), windowed_size.width, windowed_size.height);
|
||||||
}
|
}
|
||||||
|
emscripten_set_canvas_element_size(canvas_id.utf8().get_data(), windowed_size.width, windowed_size.height);
|
||||||
just_exited_fullscreen = false;
|
just_exited_fullscreen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int canvas[2];
|
int canvas[2];
|
||||||
emscripten_get_canvas_element_size(GODOT_CANVAS_SELECTOR, canvas, canvas + 1);
|
emscripten_get_canvas_element_size(canvas_id.utf8().get_data(), canvas, canvas + 1);
|
||||||
video_mode.width = canvas[0];
|
video_mode.width = canvas[0];
|
||||||
video_mode.height = canvas[1];
|
video_mode.height = canvas[1];
|
||||||
if (!window_maximized && !video_mode.fullscreen && !just_exited_fullscreen && !entering_fullscreen) {
|
if (!window_maximized && !video_mode.fullscreen && !just_exited_fullscreen && !entering_fullscreen) {
|
||||||
|
@ -1127,16 +1156,54 @@ void OS_JavaScript::delete_main_loop() {
|
||||||
memdelete(main_loop);
|
memdelete(main_loop);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OS_JavaScript::finalize_async() {
|
||||||
|
EM_ASM({
|
||||||
|
const canvas = Module['canvas'];
|
||||||
|
Object.entries(Module.listeners).forEach(function(kv) {
|
||||||
|
if (kv[0] == 'paste') {
|
||||||
|
window.removeEventListener(kv[0], kv[1], true);
|
||||||
|
} else {
|
||||||
|
canvas.removeEventListener(kv[0], kv[1]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Module.listeners = {};
|
||||||
|
Module.resize_observer.unobserve(canvas);
|
||||||
|
delete Module.resize_observer;
|
||||||
|
});
|
||||||
|
audio_driver_javascript.finish_async();
|
||||||
|
}
|
||||||
|
|
||||||
void OS_JavaScript::finalize() {
|
void OS_JavaScript::finalize() {
|
||||||
|
|
||||||
memdelete(input);
|
memdelete(input);
|
||||||
|
visual_server->finish();
|
||||||
|
emscripten_webgl_commit_frame();
|
||||||
|
memdelete(visual_server);
|
||||||
|
emscripten_webgl_destroy_context(webgl_ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Miscellaneous
|
// Miscellaneous
|
||||||
|
|
||||||
Error OS_JavaScript::execute(const String &p_path, const List<String> &p_arguments, bool p_blocking, ProcessID *r_child_id, String *r_pipe, int *r_exitcode, bool read_stderr, Mutex *p_pipe_mutex) {
|
Error OS_JavaScript::execute(const String &p_path, const List<String> &p_arguments, bool p_blocking, ProcessID *r_child_id, String *r_pipe, int *r_exitcode, bool read_stderr, Mutex *p_pipe_mutex) {
|
||||||
|
|
||||||
ERR_FAIL_V_MSG(ERR_UNAVAILABLE, "OS::execute() is not available on the HTML5 platform.");
|
Array args;
|
||||||
|
for (const List<String>::Element *E = p_arguments.front(); E; E = E->next()) {
|
||||||
|
args.push_back(E->get());
|
||||||
|
}
|
||||||
|
String json_args = JSON::print(args);
|
||||||
|
/* clang-format off */
|
||||||
|
int failed = EM_ASM_INT({
|
||||||
|
const json_args = UTF8ToString($0);
|
||||||
|
const args = JSON.parse(json_args);
|
||||||
|
if (Module["onExecute"]) {
|
||||||
|
Module["onExecute"](args);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}, json_args.utf8().get_data());
|
||||||
|
/* clang-format on */
|
||||||
|
ERR_FAIL_COND_V_MSG(failed, ERR_UNAVAILABLE, "OS::execute() must be implemented in Javascript via 'engine.setOnExecute' if required.");
|
||||||
|
return OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
Error OS_JavaScript::kill(const ProcessID &p_pid) {
|
Error OS_JavaScript::kill(const ProcessID &p_pid) {
|
||||||
|
@ -1154,7 +1221,9 @@ extern "C" EMSCRIPTEN_KEEPALIVE void send_notification(int p_notification) {
|
||||||
if (p_notification == MainLoop::NOTIFICATION_WM_MOUSE_ENTER || p_notification == MainLoop::NOTIFICATION_WM_MOUSE_EXIT) {
|
if (p_notification == MainLoop::NOTIFICATION_WM_MOUSE_ENTER || p_notification == MainLoop::NOTIFICATION_WM_MOUSE_EXIT) {
|
||||||
cursor_inside_canvas = p_notification == MainLoop::NOTIFICATION_WM_MOUSE_ENTER;
|
cursor_inside_canvas = p_notification == MainLoop::NOTIFICATION_WM_MOUSE_ENTER;
|
||||||
}
|
}
|
||||||
OS_JavaScript::get_singleton()->get_main_loop()->notification(p_notification);
|
MainLoop *loop = OS_JavaScript::get_singleton()->get_main_loop();
|
||||||
|
if (loop)
|
||||||
|
loop->notification(p_notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OS_JavaScript::_check_internal_feature_support(const String &p_feature) {
|
bool OS_JavaScript::_check_internal_feature_support(const String &p_feature) {
|
||||||
|
@ -1173,7 +1242,7 @@ bool OS_JavaScript::_check_internal_feature_support(const String &p_feature) {
|
||||||
void OS_JavaScript::alert(const String &p_alert, const String &p_title) {
|
void OS_JavaScript::alert(const String &p_alert, const String &p_title) {
|
||||||
|
|
||||||
/* clang-format off */
|
/* clang-format off */
|
||||||
EM_ASM_({
|
EM_ASM({
|
||||||
window.alert(UTF8ToString($0));
|
window.alert(UTF8ToString($0));
|
||||||
}, p_alert.utf8().get_data());
|
}, p_alert.utf8().get_data());
|
||||||
/* clang-format on */
|
/* clang-format on */
|
||||||
|
@ -1182,7 +1251,7 @@ void OS_JavaScript::alert(const String &p_alert, const String &p_title) {
|
||||||
void OS_JavaScript::set_window_title(const String &p_title) {
|
void OS_JavaScript::set_window_title(const String &p_title) {
|
||||||
|
|
||||||
/* clang-format off */
|
/* clang-format off */
|
||||||
EM_ASM_({
|
EM_ASM({
|
||||||
document.title = UTF8ToString($0);
|
document.title = UTF8ToString($0);
|
||||||
}, p_title.utf8().get_data());
|
}, p_title.utf8().get_data());
|
||||||
/* clang-format on */
|
/* clang-format on */
|
||||||
|
@ -1221,7 +1290,7 @@ void OS_JavaScript::set_icon(const Ref<Image> &p_icon) {
|
||||||
|
|
||||||
r = png.read();
|
r = png.read();
|
||||||
/* clang-format off */
|
/* clang-format off */
|
||||||
EM_ASM_ARGS({
|
EM_ASM({
|
||||||
var PNG_PTR = $0;
|
var PNG_PTR = $0;
|
||||||
var PNG_LEN = $1;
|
var PNG_LEN = $1;
|
||||||
|
|
||||||
|
@ -1248,7 +1317,7 @@ Error OS_JavaScript::shell_open(String p_uri) {
|
||||||
|
|
||||||
// Open URI in a new tab, browser will deal with it by protocol.
|
// Open URI in a new tab, browser will deal with it by protocol.
|
||||||
/* clang-format off */
|
/* clang-format off */
|
||||||
EM_ASM_({
|
EM_ASM({
|
||||||
window.open(UTF8ToString($0), '_blank');
|
window.open(UTF8ToString($0), '_blank');
|
||||||
}, p_uri.utf8().get_data());
|
}, p_uri.utf8().get_data());
|
||||||
/* clang-format on */
|
/* clang-format on */
|
||||||
|
@ -1270,26 +1339,36 @@ String OS_JavaScript::get_user_data_dir() const {
|
||||||
return "/userfs";
|
return "/userfs";
|
||||||
};
|
};
|
||||||
|
|
||||||
String OS_JavaScript::get_resource_dir() const {
|
String OS_JavaScript::get_cache_path() const {
|
||||||
|
|
||||||
return "/";
|
return "/home/web_user/.cache";
|
||||||
|
}
|
||||||
|
|
||||||
|
String OS_JavaScript::get_config_path() const {
|
||||||
|
|
||||||
|
return "/home/web_user/.config";
|
||||||
|
}
|
||||||
|
|
||||||
|
String OS_JavaScript::get_data_path() const {
|
||||||
|
|
||||||
|
return "/home/web_user/.local/share";
|
||||||
}
|
}
|
||||||
|
|
||||||
OS::PowerState OS_JavaScript::get_power_state() {
|
OS::PowerState OS_JavaScript::get_power_state() {
|
||||||
|
|
||||||
WARN_PRINT("Power management is not supported for the HTML5 platform, defaulting to POWERSTATE_UNKNOWN");
|
WARN_PRINT_ONCE("Power management is not supported for the HTML5 platform, defaulting to POWERSTATE_UNKNOWN");
|
||||||
return OS::POWERSTATE_UNKNOWN;
|
return OS::POWERSTATE_UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
int OS_JavaScript::get_power_seconds_left() {
|
int OS_JavaScript::get_power_seconds_left() {
|
||||||
|
|
||||||
WARN_PRINT("Power management is not supported for the HTML5 platform, defaulting to -1");
|
WARN_PRINT_ONCE("Power management is not supported for the HTML5 platform, defaulting to -1");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
int OS_JavaScript::get_power_percent_left() {
|
int OS_JavaScript::get_power_percent_left() {
|
||||||
|
|
||||||
WARN_PRINT("Power management is not supported for the HTML5 platform, defaulting to -1");
|
WARN_PRINT_ONCE("Power management is not supported for the HTML5 platform, defaulting to -1");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1336,6 +1415,7 @@ OS_JavaScript::OS_JavaScript(int p_argc, char *p_argv[]) {
|
||||||
transparency_enabled = false;
|
transparency_enabled = false;
|
||||||
|
|
||||||
main_loop = NULL;
|
main_loop = NULL;
|
||||||
|
visual_server = NULL;
|
||||||
|
|
||||||
idb_available = false;
|
idb_available = false;
|
||||||
sync_wait_time = -1;
|
sync_wait_time = -1;
|
||||||
|
|
|
@ -48,6 +48,8 @@ class OS_JavaScript : public OS_Unix {
|
||||||
bool just_exited_fullscreen;
|
bool just_exited_fullscreen;
|
||||||
bool transparency_enabled;
|
bool transparency_enabled;
|
||||||
|
|
||||||
|
EMSCRIPTEN_WEBGL_CONTEXT_HANDLE webgl_ctx;
|
||||||
|
|
||||||
InputDefault *input;
|
InputDefault *input;
|
||||||
Ref<InputEventKey> deferred_key_event;
|
Ref<InputEventKey> deferred_key_event;
|
||||||
CursorShape cursor_shape;
|
CursorShape cursor_shape;
|
||||||
|
@ -62,6 +64,7 @@ class OS_JavaScript : public OS_Unix {
|
||||||
MainLoop *main_loop;
|
MainLoop *main_loop;
|
||||||
int video_driver_index;
|
int video_driver_index;
|
||||||
AudioDriverJavaScript audio_driver_javascript;
|
AudioDriverJavaScript audio_driver_javascript;
|
||||||
|
VisualServer *visual_server;
|
||||||
|
|
||||||
bool idb_available;
|
bool idb_available;
|
||||||
int64_t sync_wait_time;
|
int64_t sync_wait_time;
|
||||||
|
@ -84,8 +87,6 @@ class OS_JavaScript : public OS_Unix {
|
||||||
static EM_BOOL gamepad_change_callback(int p_event_type, const EmscriptenGamepadEvent *p_event, void *p_user_data);
|
static EM_BOOL gamepad_change_callback(int p_event_type, const EmscriptenGamepadEvent *p_event, void *p_user_data);
|
||||||
void process_joypads();
|
void process_joypads();
|
||||||
|
|
||||||
static void main_loop_callback();
|
|
||||||
|
|
||||||
static void file_access_close_callback(const String &p_file, int p_flags);
|
static void file_access_close_callback(const String &p_file, int p_flags);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
@ -102,9 +103,13 @@ protected:
|
||||||
virtual bool _check_internal_feature_support(const String &p_feature);
|
virtual bool _check_internal_feature_support(const String &p_feature);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
String canvas_id;
|
||||||
|
void finalize_async();
|
||||||
|
|
||||||
// Override return type to make writing static callbacks less tedious.
|
// Override return type to make writing static callbacks less tedious.
|
||||||
static OS_JavaScript *get_singleton();
|
static OS_JavaScript *get_singleton();
|
||||||
|
|
||||||
|
virtual void swap_buffers();
|
||||||
virtual void set_video_mode(const VideoMode &p_video_mode, int p_screen = 0);
|
virtual void set_video_mode(const VideoMode &p_video_mode, int p_screen = 0);
|
||||||
virtual VideoMode get_video_mode(int p_screen = 0) const;
|
virtual VideoMode get_video_mode(int p_screen = 0) const;
|
||||||
virtual void get_fullscreen_mode_list(List<VideoMode> *p_list, int p_screen = 0) const;
|
virtual void get_fullscreen_mode_list(List<VideoMode> *p_list, int p_screen = 0) const;
|
||||||
|
@ -142,7 +147,6 @@ public:
|
||||||
virtual String get_clipboard() const;
|
virtual String get_clipboard() const;
|
||||||
|
|
||||||
virtual MainLoop *get_main_loop() const;
|
virtual MainLoop *get_main_loop() const;
|
||||||
void run_async();
|
|
||||||
bool main_loop_iterate();
|
bool main_loop_iterate();
|
||||||
|
|
||||||
virtual Error execute(const String &p_path, const List<String> &p_arguments, bool p_blocking = true, ProcessID *r_child_id = NULL, String *r_pipe = NULL, int *r_exitcode = NULL, bool read_stderr = false, Mutex *p_pipe_mutex = NULL);
|
virtual Error execute(const String &p_path, const List<String> &p_arguments, bool p_blocking = true, ProcessID *r_child_id = NULL, String *r_pipe = NULL, int *r_exitcode = NULL, bool read_stderr = false, Mutex *p_pipe_mutex = NULL);
|
||||||
|
@ -157,7 +161,9 @@ public:
|
||||||
virtual String get_name() const;
|
virtual String get_name() const;
|
||||||
virtual bool can_draw() const;
|
virtual bool can_draw() const;
|
||||||
|
|
||||||
virtual String get_resource_dir() const;
|
virtual String get_cache_path() const;
|
||||||
|
virtual String get_config_path() const;
|
||||||
|
virtual String get_data_path() const;
|
||||||
virtual String get_user_data_dir() const;
|
virtual String get_user_data_dir() const;
|
||||||
|
|
||||||
virtual OS::PowerState get_power_state();
|
virtual OS::PowerState get_power_state();
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
var Engine = {
|
|
||||||
RuntimeEnvironment: function(Module, exposedLibs) {
|
|
||||||
// The above is concatenated with generated code, and acts as the start of
|
|
||||||
// a wrapper for said code. See engine.js for the other part of the
|
|
||||||
// wrapper.
|
|
Loading…
Reference in New Issue