Compare commits

..
5 Commits
9 changed files with 130 additions and 13 deletions

No files matched your search

+10 -3
View File
@@ -318,8 +318,12 @@ fetches) to the server, so the game talks to its own origin; the server stays un
(IndexedDB), X-MM-User sent as on desktop (the cookie works too, same origin). (IndexedDB), X-MM-User sent as on desktop (the cookie works too, same origin).
Fullscreen (F11, display dialog) through the browser's Fullscreen API, not saved or Fullscreen (F11, display dialog) through the browser's Fullscreen API, not saved or
restored (the browser allows it only after a click or key). Moon generation is plain restored (the browser allows it only after a click or key). Moon generation is plain
Lua there (no LuaJIT): grid capped at 96 (`WEB_RESOLUTION`) and 12 ms a frame, a few Lua there (no LuaJIT): grid capped at 96 (`WEB_RESOLUTION`) and 8 ms a frame (keeps
seconds per moon. Moon targets fall back to 8 bits without EXT_color_buffer_float. 60 fps; a checkpoint every few ms of work in terrain.lua), about 4 s per moon. Moon targets fall back to 8 bits without EXT_color_buffer_float.
Variable frame rate there (`sys.set_update_frequency(0)` in main.script): the browser's
requestAnimationFrame paces at the display's rate; `display.update_frequency = 60`
(desktop's timer pacing) made the engine skip callbacks: 60 fps in a 2/3 cadence on
120/144 Hz displays, dropped frames on jittery 60 Hz ones. Use dt or the clock for motion.
- WebGL: every sampler of a bound program must not be the framebuffer being drawn - WebGL: every sampler of a bound program must not be the framebuffer being drawn
("feedback loop", the draw is dropped), and Firefox aborts on resizing a target with a ("feedback loop", the draw is dropped), and Firefox aborts on resizing a target with a
depth buffer: the render script binds every bloom sampler and makes targets again at a depth buffer: the render script binds every bloom sampler and makes targets again at a
@@ -377,7 +381,10 @@ The game it starts saves its files under `<out>/data` (XDG_DATA_HOME, or
`g`, `st`, `view`, `busy`, `el(id)`), `idle()` (no request in flight, built), `g`, `st`, `view`, `busy`, `el(id)`), `idle()` (no request in flight, built),
`eval(expr)`, `elements()`, `state()`, `set("reduced_motion", "1")`, `eval(expr)`, `elements()`, `state()`, `set("reduced_motion", "1")`,
`set("scroll:<key>", px)`, `shot(name)`, `log_has(regex)`, `expect(cond, msg)`, `set("scroll:<key>", px)`, `shot(name)`, `log_has(regex)`, `expect(cond, msg)`,
`restart()` (quit and start the game again, same options and saved files). `restart()` (quit and start the game again, same options and saved files),
`cmd("frames", "reset")` (frame times since the last reset: fps, dt, the game screen's
update and its rebuilds in ms; the web harness reads it the same way). F3 in a debug
build shows the engine's profiler.
Those go through the test hook, which calls the screen's input function directly, so Those go through the test hook, which calls the screen's input function directly, so
they work even when the engine sends the game no input. Real input through the X server they work even when the engine sends the game no input. Real input through the X server
(xdotool on the driver's Xvfb; key and button held 80 ms, since the engine polls once a (xdotool on the driver's Xvfb; key and button held 80 ms, since the engine polls once a
+7
View File
@@ -26,6 +26,13 @@ end
function init(self) function init(self)
print("mm: api " .. api.base .. ", player " .. (api.player_id() or "none yet")) print("mm: api " .. api.base .. ", player " .. (api.player_id() or "none yet"))
-- In a browser the frames come from requestAnimationFrame at the display's rate; with
-- display.update_frequency set the engine skips callbacks to approach it (judder on
-- 120/144 Hz displays, dropped frames on jittery 60 Hz ones). Variable rate there:
-- every callback is a frame, dt is the real time.
if html5 then
sys.set_update_frequency(0)
end
local w, h = sys.get_config_string("mm.window", ""):match("^(%d+)x(%d+)$") local w, h = sys.get_config_string("mm.window", ""):match("^(%d+)x(%d+)$")
if w then if w then
msg.post("@render:", "resize", { width = tonumber(w), height = tonumber(h) }) msg.post("@render:", "resize", { width = tonumber(w), height = tonumber(h) })
+27 -2
View File
@@ -424,7 +424,13 @@ local FACES = {
-- Unit-sphere directions of the cube-sphere grid (6 faces of (n+1)^2 vertices, flat xyz, -- Unit-sphere directions of the cube-sphere grid (6 faces of (n+1)^2 vertices, flat xyz,
-- vertex v at [3v+1..3v+3], v 0-based) and the shared key of each seam vertex (-1 inside). -- vertex v at [3v+1..3v+3], v 0-based) and the shared key of each seam vertex (-1 inside).
-- (Read-only for callers: cached per n, like the strip.)
local sphere_cache = {}
function M.cube_sphere(n) function M.cube_sphere(n)
local hit = sphere_cache[n]
if hit then
return hit[1], hit[2]
end
local row = n + 1 local row = n + 1
local dirs, keys = {}, {} local dirs, keys = {}, {}
local v = 0 local v = 0
@@ -453,6 +459,7 @@ function M.cube_sphere(n)
end end
end end
end end
sphere_cache[n] = { dirs, keys }
return dirs, keys return dirs, keys
end end
@@ -539,6 +546,7 @@ function M.generate(p, checkpoint)
local per = row * row local per = row * row
local count = 6 * per local count = 6 * per
local dirs, keys = M.cube_sphere(n) local dirs, keys = M.cube_sphere(n)
checkpoint()
local heights, ejecta = {}, {} local heights, ejecta = {}, {}
local technique = p.technique local technique = p.technique
@@ -553,6 +561,7 @@ function M.generate(p, checkpoint)
cparams.maxRadius = cr.maxRadius * 0.6 cparams.maxRadius = cr.maxRadius * 0.6
end end
local craters = make_craters(cparams, p.craterSeed or 1, crater_field and 3 or 1) local craters = make_craters(cparams, p.craterSeed or 1, crater_field and 3 or 1)
checkpoint()
local x_max = craters.x_max local x_max = craters.x_max
local shape_strength = crater_field and sh.strength * 0.4 or sh.strength local shape_strength = crater_field and sh.strength * 0.4 or sh.strength
local smooth = max(0, cr.smoothness) local smooth = max(0, cr.smoothness)
@@ -615,7 +624,7 @@ function M.generate(p, checkpoint)
end end
heights[i + 1] = h + crater_h * cr.depth heights[i + 1] = h + crater_h * cr.depth
ejecta[i + 1] = min(1, glow) ejecta[i + 1] = min(1, glow)
if i % 256 == 0 then if i % 64 == 0 then
checkpoint() checkpoint()
end end
end end
@@ -624,6 +633,9 @@ function M.generate(p, checkpoint)
local mean = 0 local mean = 0
for i = 1, count do for i = 1, count do
mean = mean + heights[i] mean = mean + heights[i]
if i % 4096 == 0 then
checkpoint()
end
end end
mean = mean / count mean = mean / count
local positions = {} local positions = {}
@@ -641,6 +653,9 @@ function M.generate(p, checkpoint)
if hh > max_h then if hh > max_h then
max_h = hh max_h = hh
end end
if i % 2048 == 0 then
checkpoint()
end
end end
checkpoint() checkpoint()
@@ -648,6 +663,9 @@ function M.generate(p, checkpoint)
local normals = {} local normals = {}
for i = 1, count * 3 do for i = 1, count * 3 do
normals[i] = 0 normals[i] = 0
if i % 8192 == 0 then
checkpoint()
end
end end
local P = positions local P = positions
local function tri(a, b, c) local function tri(a, b, c)
@@ -690,7 +708,11 @@ function M.generate(p, checkpoint)
seams[key] = { i } seams[key] = { i }
end end
end end
if i % 4096 == 0 then
checkpoint()
end end
end
checkpoint()
for _, group in pairs(seams) do for _, group in pairs(seams) do
if #group > 1 then if #group > 1 then
local sx, sy, sz = 0, 0, 0 local sx, sy, sz = 0, 0, 0
@@ -710,6 +732,9 @@ function M.generate(p, checkpoint)
len = 1 len = 1
end end
normals[o + 1], normals[o + 2], normals[o + 3] = nx / len, ny / len, nz / len normals[o + 1], normals[o + 2], normals[o + 3] = nx / len, ny / len, nz / len
if i % 2048 == 0 then
checkpoint()
end
end end
checkpoint() checkpoint()
@@ -755,7 +780,7 @@ function M.generate(p, checkpoint)
local gr = 1 + fbm(x + ox, y + oy, z + oz, 2, 14, 0.5, 2) * grain local gr = 1 + fbm(x + ox, y + oy, z + oz, 2, 14, 0.5, 2) * grain
colors[o + 1], colors[o + 2], colors[o + 3] = c1 * gr, c2 * gr, c3 * gr colors[o + 1], colors[o + 2], colors[o + 3] = c1 * gr, c2 * gr, c3 * gr
if i % 512 == 0 then if i % 128 == 0 then
checkpoint() checkpoint()
end end
end end
+61
View File
@@ -24,6 +24,9 @@
-- state a summary of the shown state (view, wallet, counts, busy) -- state a summary of the shown state (view, wallet, counts, busy)
-- set <k> <v> settings: reduced_motion, theme (dark|light), scroll:<key> -- set <k> <v> settings: reduced_motion, theme (dark|light), scroll:<key>
-- mark <name> print "mm: shot <name>" (scripts/shot.sh SHOTS_DIR picks it up) -- mark <name> print "mm: shot <name>" (scripts/shot.sh SHOTS_DIR picks it up)
-- frames [reset] frame times since the last reset (the last 600 frames): fps, dt,
-- the game screen's update and its rebuilds, in ms (n, mean, p50,
-- p95, max); "reset" starts over
local focus = require("modules.focus") local focus = require("modules.focus")
@@ -427,6 +430,64 @@ function COMMANDS.set(g, cmd, _, api)
return { ok = true } return { ok = true }
end end
-- Frame times (the frames command): the engine's dt, the game screen's update time and
-- the rebuilds' time, recorded by M.frame every frame (core/main.lua) with M.clock (on the
-- web the page's performance.now: socket.gettime has whole milliseconds there).
local FRAMES = 600
local frames
local function frames_reset()
frames = { n = 0, i = 0, dt = {}, lua = {}, builds = {} }
end
frames_reset()
function M.clock()
if html5 then
return (tonumber(html5.run("performance.now()")) or 0) / 1000
end
return socket.gettime()
end
function M.frame(dt, lua_secs, build_secs)
frames.i = frames.i % FRAMES + 1
frames.n = math.min(frames.n + 1, FRAMES)
frames.dt[frames.i] = dt
frames.lua[frames.i] = lua_secs
if build_secs and #frames.builds < FRAMES then
frames.builds[#frames.builds + 1] = build_secs
end
end
local function stats(list, n)
local t, sum = {}, 0
for i = 1, n do
t[i] = list[i] * 1000
sum = sum + t[i]
end
table.sort(t)
local function r(v)
return math.floor(v * 100 + 0.5) / 100
end
local function q(p)
return n > 0 and r(t[math.min(n, math.floor(p * n) + 1)]) or 0
end
return { n = n, mean = n > 0 and r(sum / n) or 0, p50 = q(0.5), p95 = q(0.95), max = q(1) }
end
function COMMANDS.frames(g, cmd)
local n = frames.n
local total = 0
for i = 1, n do
total = total + frames.dt[i]
end
local res = { ok = true, fps = total > 0 and math.floor(n / total * 10 + 0.5) / 10 or 0,
dt = stats(frames.dt, n), lua = stats(frames.lua, n), build = stats(frames.builds, #frames.builds) }
if cmd.args[1] == "reset" then
frames_reset()
end
return res
end
function COMMANDS.mark(g, cmd) function COMMANDS.mark(g, cmd)
print("mm: shot " .. tostring(cmd.args[1])) print("mm: shot " .. tostring(cmd.args[1]))
return { ok = true } return { ok = true }
+1 -1
View File
@@ -1,7 +1,7 @@
#version 140 #version 140
// The viewport node: shows the moon scene the render script drew into `moon_tex` (HDR, // The viewport node: shows the moon scene the render script drew into `moon_tex` (HDR,
// 2x the node's size: linear filtering at the pixel centers averages 2x2 samples) plus // up to 2x the node's size: linear filtering at the pixel centers averages 2x2 samples) plus
// its bloom (`bloom_tex`, half size, upsampled), with three.js's output pass: ACES // its bloom (`bloom_tex`, half size, upsampled), with three.js's output pass: ACES
// filmic tone mapping (exposure) and the sRGB transfer. // filmic tone mapping (exposure) and the sRGB transfer.
// Without a moon (moon_rect.z == 0) the node keeps its plain color. // Without a moon (moon_rect.z == 0) the node keeps its plain color.
+10 -5
View File
@@ -24,10 +24,13 @@ local FOV = math.rad(35)
local RESUME = 1.5 -- seconds before auto-rotation resumes after a drag local RESUME = 1.5 -- seconds before auto-rotation resumes after a drag
local ROTATE_SPEED = 0.6 local ROTATE_SPEED = 0.6
local DAMPING = 0.05 local DAMPING = 0.05
local BUDGET = html5 and 0.012 or 0.006 -- seconds of generation work per frame (the web runs local BUDGET = html5 and 0.008 or 0.006 -- seconds of generation work per frame (the web runs
-- plain Lua, no LuaJIT: several times slower, so it gets more of each frame) -- plain Lua, no LuaJIT: several times slower, so it gets more of each frame; 12 ms there
-- overran 60 fps frames: 33 fps for the same ~4 s)
local CACHE_SIZE = 6 local CACHE_SIZE = 6
local SUPERSAMPLE = 2 -- the scene renders at 2x and is averaged down (antialiasing) local SUPERSAMPLE = 2 -- the scene renders at 2x and is averaged down (antialiasing),
-- counted in GUI reference pixels: a HiDPI display (or UI zoom) already gives the viewport
-- more window pixels, so the target is 2x the reference size, at least the window size
local CHARGE = 1.1 -- reveal: charge-up before the burst (s) local CHARGE = 1.1 -- reveal: charge-up before the burst (s)
local BURST = 0.9 -- reveal: the burst itself (s) local BURST = 0.9 -- reveal: the burst itself (s)
local TIMEOUT = 4 -- reveal: burst without the mesh after CHARGE + this (s) local TIMEOUT = 4 -- reveal: burst without the mesh after CHARGE + this (s)
@@ -199,7 +202,9 @@ local function start_job(self, id, key, params)
end end
local m = terrain.generate(params, checkpoint) local m = terrain.generate(params, checkpoint)
job.gen_work = job.work + (now() - job.resume_at) job.gen_work = job.work + (now() - job.resume_at)
checkpoint()
local strip = terrain.strip(m.n) local strip = terrain.strip(m.n)
checkpoint()
local buf = buffer.create(#strip, STREAMS) local buf = buffer.create(#strip, STREAMS)
local pos = buffer.get_stream(buf, hash("position")) local pos = buffer.get_stream(buf, hash("position"))
local nor = buffer.get_stream(buf, hash("normal")) local nor = buffer.get_stream(buf, hash("normal"))
@@ -528,13 +533,13 @@ function update(self, dt)
self.spin = self.spin + self.spin_speed * dt self.spin = self.spin + self.spin_speed * dt
go.set_rotation(TILT * vmath.quat_rotation_y(self.spin), "surface") go.set_rotation(TILT * vmath.quat_rotation_y(self.spin), "surface")
local ss = SUPERSAMPLE local ss = math.max(1, SUPERSAMPLE / math.max(1, moon.unit))
local aspect = rect.w / rect.h local aspect = rect.w / rect.h
local eye = update_camera(self, aspect) local eye = update_camera(self, aspect)
f.visible = values ~= nil f.visible = values ~= nil
f.has_mesh = self.shown_key ~= nil and show_moon f.has_mesh = self.shown_key ~= nil and show_moon
f.rect = rect f.rect = rect
f.rt_w, f.rt_h = rect.w * ss, rect.h * ss f.rt_w, f.rt_h = math.floor(rect.w * ss + 0.5), math.floor(rect.h * ss + 0.5)
f.unit = moon.unit * ss f.unit = moon.unit * ss
f.view = vmath.matrix4_look_at(eye, vmath.vector3(0, 0, 0), vmath.vector3(0, 1, 0)) f.view = vmath.matrix4_look_at(eye, vmath.vector3(0, 0, 0), vmath.vector3(0, 1, 0))
f.proj = vmath.matrix4_perspective(self.fov, aspect, 0.1, 500) f.proj = vmath.matrix4_perspective(self.fov, aspect, 0.1, 500)
+1 -1
View File
@@ -1,7 +1,7 @@
-- Moon Miners render script: Defold's default render script (builtins -- Moon Miners render script: Defold's default render script (builtins
-- default.render_script of the pinned engine) plus the moon pass. Before the GUI, the moon -- default.render_script of the pinned engine) plus the moon pass. Before the GUI, the moon
-- scene (moon/moon.collection, see modules/moon/moon.lua) is drawn into an HDR render -- scene (moon/moon.collection, see modules/moon/moon.lua) is drawn into an HDR render
-- target at 2x the viewport size: backdrop, stars, the moon, the atmosphere shell; then -- target at 2x the viewport's GUI reference size (at least its window size; moon.script): backdrop, stars, the moon, the atmosphere shell; then
-- its bloom (three.js UnrealBloomPass) into smaller targets. The GUI then draws it in its viewport node (material moon/materials/moon_gui, sampler -- its bloom (three.js UnrealBloomPass) into smaller targets. The GUI then draws it in its viewport node (material moon/materials/moon_gui, sampler
-- `moon_tex`), tone mapped, so HUD panels, captions and popups layer over it as usual. -- `moon_tex`), tone mapped, so HUD panels, captions and popups layer over it as usual.
+7 -1
View File
@@ -8,7 +8,7 @@
-- the focus ring) scroll the scroll area under the mouse -- the focus ring) scroll the scroll area under the mouse
-- (else the one holding the focused element, else the biggest one), like a browser. -- (else the one holding the focused element, else the biggest one), like a browser.
-- F11 toggles fullscreen; Ctrl + / Ctrl - / Ctrl 0 change the UI scale (modules/screen), -- F11 toggles fullscreen; Ctrl + / Ctrl - / Ctrl 0 change the UI scale (modules/screen),
-- like the browser's zoom. -- like the browser's zoom. F3 (debug builds) shows the engine's profiler (frame times).
-- --
-- Text fields made in a build register in g.inputs ({ input = Druid input, submit = fn, -- Text fields made in a build register in g.inputs ({ input = Druid input, submit = fn,
-- step = { min, max } }: modules/field, the refinery's quantity); core/main resets the list -- step = { min, max } }: modules/field, the refinery's quantity); core/main resets the list
@@ -30,6 +30,7 @@ local SHIFT = { [hash("key_lshift")] = true, [hash("key_rshift")] = true }
local ALT = { [hash("key_lalt")] = true, [hash("key_ralt")] = true } local ALT = { [hash("key_lalt")] = true, [hash("key_ralt")] = true }
local CTRL = { [hash("key_lctrl")] = true, [hash("key_rctrl")] = true } local CTRL = { [hash("key_lctrl")] = true, [hash("key_rctrl")] = true }
local F11 = hash("key_f11") local F11 = hash("key_f11")
local F3 = hash("key_f3")
local ESC = hash("key_esc") local ESC = hash("key_esc")
local ZOOM = { [hash("key_plus")] = 1, [hash("key_equals")] = 1, [hash("key_numpad_add")] = 1, local ZOOM = { [hash("key_plus")] = 1, [hash("key_equals")] = 1, [hash("key_numpad_add")] = 1,
[hash("key_minus")] = -1, [hash("key_numpad_subtract")] = -1, [hash("key_0")] = 0, [hash("key_numpad_0")] = 0 } [hash("key_minus")] = -1, [hash("key_numpad_subtract")] = -1, [hash("key_0")] = 0, [hash("key_numpad_0")] = 0 }
@@ -172,6 +173,11 @@ function M.before(g, action_id, action, input)
screen.toggle_fullscreen() screen.toggle_fullscreen()
return true return true
end end
if action_id == F3 and action.pressed and profiler and sys.get_engine_info().is_debug then
g.profiler_ui = not g.profiler_ui
profiler.enable_ui(g.profiler_ui)
return true
end
if g.mods.ctrl and action_id and ZOOM[action_id] and action.pressed then if g.mods.ctrl and action_id and ZOOM[action_id] and action.pressed then
screen.step_ui_scale(ZOOM[action_id]) screen.step_ui_scale(ZOOM[action_id])
return true return true
+6
View File
@@ -465,9 +465,12 @@ local TEST_API = {
} }
function M.update(g, dt) function M.update(g, dt)
local t0 = test.enabled and test.clock()
local built
local W, H, scale = screen.layout() local W, H, scale = screen.layout()
if g.dirty or W ~= g.size[1] or H ~= g.size[2] or scale ~= g.size[3] then if g.dirty or W ~= g.size[1] or H ~= g.size[2] or scale ~= g.size[3] then
M.build(g) M.build(g)
built = t0 and test.clock() - t0
end end
if g.boot_tick and not g.state then if g.boot_tick and not g.state then
local left = math.max(0, math.ceil(g.boot_tick.at - socket.gettime())) local left = math.max(0, math.ceil(g.boot_tick.at - socket.gettime()))
@@ -505,6 +508,9 @@ function M.update(g, dt)
g.autoplay:update(dt) g.autoplay:update(dt)
end end
test.update(g, function(id, a) return input(g, id, a) end, TEST_API) test.update(g, function(id, a) return input(g, id, a) end, TEST_API)
if t0 then
test.frame(dt, test.clock() - t0, built)
end
end end
function M.on_message(g, message_id, message, sender) function M.on_message(g, message_id, message, sender)