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).
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
Lua there (no LuaJIT): grid capped at 96 (`WEB_RESOLUTION`) and 12 ms a frame, a few
seconds per moon. Moon targets fall back to 8 bits without EXT_color_buffer_float.
Lua there (no LuaJIT): grid capped at 96 (`WEB_RESOLUTION`) and 8 ms a frame (keeps
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
("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
@@ -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),
`eval(expr)`, `elements()`, `state()`, `set("reduced_motion", "1")`,
`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
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
+7
View File
@@ -26,6 +26,13 @@ end
function init(self)
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+)$")
if w then
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,
-- 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)
local hit = sphere_cache[n]
if hit then
return hit[1], hit[2]
end
local row = n + 1
local dirs, keys = {}, {}
local v = 0
@@ -453,6 +459,7 @@ function M.cube_sphere(n)
end
end
end
sphere_cache[n] = { dirs, keys }
return dirs, keys
end
@@ -539,6 +546,7 @@ function M.generate(p, checkpoint)
local per = row * row
local count = 6 * per
local dirs, keys = M.cube_sphere(n)
checkpoint()
local heights, ejecta = {}, {}
local technique = p.technique
@@ -553,6 +561,7 @@ function M.generate(p, checkpoint)
cparams.maxRadius = cr.maxRadius * 0.6
end
local craters = make_craters(cparams, p.craterSeed or 1, crater_field and 3 or 1)
checkpoint()
local x_max = craters.x_max
local shape_strength = crater_field and sh.strength * 0.4 or sh.strength
local smooth = max(0, cr.smoothness)
@@ -615,7 +624,7 @@ function M.generate(p, checkpoint)
end
heights[i + 1] = h + crater_h * cr.depth
ejecta[i + 1] = min(1, glow)
if i % 256 == 0 then
if i % 64 == 0 then
checkpoint()
end
end
@@ -624,6 +633,9 @@ function M.generate(p, checkpoint)
local mean = 0
for i = 1, count do
mean = mean + heights[i]
if i % 4096 == 0 then
checkpoint()
end
end
mean = mean / count
local positions = {}
@@ -641,6 +653,9 @@ function M.generate(p, checkpoint)
if hh > max_h then
max_h = hh
end
if i % 2048 == 0 then
checkpoint()
end
end
checkpoint()
@@ -648,6 +663,9 @@ function M.generate(p, checkpoint)
local normals = {}
for i = 1, count * 3 do
normals[i] = 0
if i % 8192 == 0 then
checkpoint()
end
end
local P = positions
local function tri(a, b, c)
@@ -690,7 +708,11 @@ function M.generate(p, checkpoint)
seams[key] = { i }
end
end
if i % 4096 == 0 then
checkpoint()
end
end
checkpoint()
for _, group in pairs(seams) do
if #group > 1 then
local sx, sy, sz = 0, 0, 0
@@ -710,6 +732,9 @@ function M.generate(p, checkpoint)
len = 1
end
normals[o + 1], normals[o + 2], normals[o + 3] = nx / len, ny / len, nz / len
if i % 2048 == 0 then
checkpoint()
end
end
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
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()
end
end
+61
View File
@@ -24,6 +24,9 @@
-- state a summary of the shown state (view, wallet, counts, busy)
-- 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)
-- 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")
@@ -427,6 +430,64 @@ function COMMANDS.set(g, cmd, _, api)
return { ok = true }
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)
print("mm: shot " .. tostring(cmd.args[1]))
return { ok = true }
+1 -1
View File
@@ -1,7 +1,7 @@
#version 140
// 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
// filmic tone mapping (exposure) and the sRGB transfer.
// 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 ROTATE_SPEED = 0.6
local DAMPING = 0.05
local BUDGET = html5 and 0.012 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)
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; 12 ms there
-- overran 60 fps frames: 33 fps for the same ~4 s)
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 BURST = 0.9 -- reveal: the burst itself (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
local m = terrain.generate(params, checkpoint)
job.gen_work = job.work + (now() - job.resume_at)
checkpoint()
local strip = terrain.strip(m.n)
checkpoint()
local buf = buffer.create(#strip, STREAMS)
local pos = buffer.get_stream(buf, hash("position"))
local nor = buffer.get_stream(buf, hash("normal"))
@@ -528,13 +533,13 @@ function update(self, dt)
self.spin = self.spin + self.spin_speed * dt
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 eye = update_camera(self, aspect)
f.visible = values ~= nil
f.has_mesh = self.shown_key ~= nil and show_moon
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.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)
+1 -1
View File
@@ -1,7 +1,7 @@
-- 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
-- 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
-- `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
-- (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),
-- 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,
-- 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 CTRL = { [hash("key_lctrl")] = true, [hash("key_rctrl")] = true }
local F11 = hash("key_f11")
local F3 = hash("key_f3")
local ESC = hash("key_esc")
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 }
@@ -172,6 +173,11 @@ function M.before(g, action_id, action, input)
screen.toggle_fullscreen()
return true
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
screen.step_ui_scale(ZOOM[action_id])
return true
+6
View File
@@ -465,9 +465,12 @@ local TEST_API = {
}
function M.update(g, dt)
local t0 = test.enabled and test.clock()
local built
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
M.build(g)
built = t0 and test.clock() - t0
end
if g.boot_tick and not g.state then
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)
end
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
function M.on_message(g, message_id, message, sender)