2 Commits
Author SHA1 Message Date
sanspie 8391ebcd98 docs: plan the end of Stage 11
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 29m44s
ci / checks (push) Successful in 15m25s
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / release (push) Successful in 20m0s
2026-10-21 22:27:08 +03:00
sanspie 3ab43e84f1 Windows start: boot log, fatal message box, OpenGL driver error 2026-10-21 22:27:04 +03:00
10 changed files with 228 additions and 8 deletions
+2 -1
View File
@@ -550,7 +550,7 @@ The Stage 8 specification numbered these decisions D153, D155, D158, D159, D168
| ID | Decision |
|---|---|
| D327 | Desktop-first onboarding. Installing, creating and opening projects, testing and exporting need no terminal on Windows, macOS and Linux; the CLI stays for agents and CI. Every desktop entry point (the Windows shortcut and file associations for `.aego`, `.aescene`, `.aeprefab` and folders, the macOS app's document types, the Linux AppImage and the `.desktop` file from install.sh) starts the same `edit.Start` as `aego edit`: without a project it opens the launcher, a file argument opens its project and, for a scene or prefab, that file (`-open`, passed through the supervisor), a native project goes to the supervisor when Go is found and otherwise opens for scripts with a status-bar note. GUI builds write logs to the user cache (`aego/logs`, last ten) and show fatal errors in a message box; child processes are started without console windows (`app/proc`), and the supervised editor is built with `-H=windowsgui`. Native dialogs are `platform.FileDialogs` on the window, implemented with SDL's folder and file dialogs and delivered through a registered SDL event; headless windows have none and the UI falls back to a text field. The launcher creates projects in `Documents/Aego Projects` by default (XDG documents on Linux), offers native code only with Go and engine sources, opens dropped folders and `project.aego`, and has Open, Show in the file manager and Remove on recent projects. Once a day it asks the Gitea API for the latest release and shows a banner linking the download page; it never updates by itself and the check can be turned off (`UserConfigDir/aego/update`). Switching projects from the editor (New, Open, Open Recent, Close Project, and dropping another project's folder) starts the original entry point again (`AEGO_EDITOR_EXE`, `AEGO_EDITOR_VERB`, `$APPIMAGE`) without the supervised flag and quits, after asking about unsaved documents. Play → Run Script Tests runs `aego test` in-process with results in Console; the Export panel takes an output folder and can open or run the result, serving web builds on 127.0.0.1 with COOP/COEP. The home and install pages carry download buttons that gendocs renders at site build time from the latest Gitea release (`site.FetchRelease`, `downloadBlock`): one list item per file that the release has, and `site.js` turns the main button into the visitor's system, or into a link to the install page when the release has no file for it. The app icon (D301) is the window icon of the launcher and the editor through `platform.SetAppIcon`, set in `edit.Start` before the first window. |
| D327 | Desktop-first onboarding. Installing, creating and opening projects, testing and exporting need no terminal on Windows, macOS and Linux; the CLI stays for agents and CI. Every desktop entry point (the Windows shortcut and file associations for `.aego`, `.aescene`, `.aeprefab` and folders, the macOS app's document types, the Linux AppImage and the `.desktop` file from install.sh) starts the same `edit.Start` as `aego edit`: without a project it opens the launcher, a file argument opens its project and, for a scene or prefab, that file (`-open`, passed through the supervisor), a native project goes to the supervisor when Go is found and otherwise opens for scripts with a status-bar note. GUI builds write logs to the user cache (`aego/logs`, last ten) and show fatal errors in a message box; child processes are started without console windows (`app/proc`), and the supervised editor is built with `-H=windowsgui`. Native dialogs are `platform.FileDialogs` on the window, implemented with SDL's folder and file dialogs and delivered through a registered SDL event; headless windows have none and the UI falls back to a text field. The launcher creates projects in `Documents/Aego Projects` by default (XDG documents on Linux), offers native code only with Go and engine sources, opens dropped folders and `project.aego`, and has Open, Show in the file manager and Remove on recent projects. Once a day it asks the Gitea API for the latest release and shows a banner linking the download page; it never updates by itself and the check can be turned off (`UserConfigDir/aego/update`). Switching projects from the editor (New, Open, Open Recent, Close Project, and dropping another project's folder) starts the original entry point again (`AEGO_EDITOR_EXE`, `AEGO_EDITOR_VERB`, `$APPIMAGE`) without the supervised flag and quits, after asking about unsaved documents. Play → Run Script Tests runs `aego test` in-process with results in Console; the Export panel takes an output folder and can open or run the result, serving web builds on 127.0.0.1 with COOP/COEP. The home and install pages carry download buttons that gendocs renders at site build time from the latest Gitea release (`site.FetchRelease`, `downloadBlock`): one list item per file that the release has, and `site.js` turns the main button into the visitor's system, or into a link to the install page when the release has no file for it. The app icon (D301) is the window icon of the launcher and the editor through `platform.SetAppIcon`, set in `edit.Start` before the first window. `edit.Start` writes a boot log to `<user cache>/aego/logs/boot.log` (the previous run is kept as `boot.prev.log`): version, executable, arguments, working directory, Go, then each startup step (SDL init, window, GL version, launcher, project, missing Go). A failed OpenGL 3.3 core context probes a default context and names what the driver has (`gl: … driver has GDI Generic 1.1.0`). A start error or panic shows a message box with the log path; on Windows through `MessageBoxW` (`platform.ShowFatal`, usable before and after `SDL_Init`), elsewhere through the SDL alert. |
## 4. Dependency rules
@@ -1009,6 +1009,7 @@ Packages are named by capability (`render/view`, `asset/loader`, `scene/sceneio`
| 9.5c | Web export, docs session with Inspector and Game View, Try it, playground sandbox |
| 9.6 | Agent tooling: `scene/mutate` (transactions with exact inverses and redo, data documents), `project/refs`, `project/refactor`, `project/rename`, editor structure edits through mutate, `app/agent` command catalog and `aego <group> <verb>` CLI, `aego mcp`, external edits in the editor |
| 9.7 | Docs and release: editor theme and icons on the site, build-time highlighting with links, short-name references, Try it progress, cache and scene tree, Try scenes for every 2D physics component, script examples on component pages, Script API index and module examples, diagnostics with checked examples, install page, release builds and install script, editor screenshots; done |
| 11 (close) | In this order: the GitHub mirror (push mirror from Gitea; GitHub Actions run the test matrix and build every release on Linux, Windows and a native macOS runner, so the `.dmg` and signed binaries come from CI; `install.sh`, `install.ps1`, the launcher's update check and the download buttons point to GitHub releases; the docs site keeps deploying from Gitea); custom installers for Windows, macOS and Linux (own UI instead of the stock NSIS pages, a drag-to-Applications `.dmg` with a background, an AppImage with a first-run desktop integration prompt); editor telemetry, logs and crash bundles sent to akarpov.ru with opt-in consent; last, telemetry and crash collection for exported games, sent to the developer's own endpoint or to an akarpov.ru account with a limited quota |
| 13+ | Export/pak, networking/store, Android |
### Stage 9 complete
+27
View File
@@ -0,0 +1,27 @@
package edit
import (
"os"
"os/exec"
"runtime"
"strings"
"aego/engine"
"aego/platform"
)
func startBoot(verb string) {
wd, _ := os.Getwd()
exe, _ := os.Executable()
gobin, err := exec.LookPath("go")
if err != nil {
gobin = "not on PATH"
}
_ = platform.StartBoot(platform.DefaultBootPath(),
"aego "+engine.Version+" "+runtime.GOOS+"/"+runtime.GOARCH+" "+verb,
"exe "+exe,
"args "+strings.Join(os.Args[1:], " "),
"cwd "+wd,
"go "+gobin,
)
}
+6 -1
View File
@@ -61,7 +61,8 @@ func Main(mods ...sdk.Module) {
func fail(err error) {
os.Stderr.WriteString("aego-editor: " + err.Error() + "\n")
if !interactive() {
platform.Boot("fatal", err.Error())
if !platform.ShowFatal("Aego", platform.FatalText("Aego", err.Error())) && !interactive() {
platform.Alert("Aego", err.Error())
}
os.Exit(1)
@@ -112,6 +113,8 @@ func relInside(root, file string) (string, bool) {
}
func Start(e Entry) error {
defer platform.RecoverFatal("Aego")
startBoot("edit")
platform.SetAppIcon(brand.Window()...)
markEntry(e.Verb)
cfg := e.Config
@@ -145,6 +148,7 @@ func Start(e Entry) error {
cfg.OpenPath = rel
}
}
platform.Boot("editor", p.Root)
if bootstrap.NeedsNative(p) && !cfg.Headless && cfg.Frames == 0 && !cfg.Supervised {
if goFound() {
return supervise.Run(p, supervise.Options{
@@ -153,6 +157,7 @@ func Start(e Entry) error {
})
}
cfg.NativeNote = NoGoNote
platform.Boot("native", NoGoNote)
}
return Run(p, cfg, e.Mods...)
}
+1
View File
@@ -176,6 +176,7 @@ func OpenLauncher(cfg Config) (*LaunchSession, error) {
}
ls := &LaunchSession{Engine: e}
d := &launchDriver{s: ls, renderer: mod.Renderer(), log: lg.With(shell.ChannelEditor), images: map[string]launchImage{}}
platform.Boot("launcher", "open")
ls.UI = launcher.New(launcher.Options{
Templates: launcherTemplates(),
Examples: examples,
+93
View File
@@ -0,0 +1,93 @@
package platform
import (
"fmt"
"os"
"path/filepath"
"runtime/debug"
"strings"
"sync"
"time"
)
var boot struct {
mu sync.Mutex
f *os.File
path string
}
func DefaultBootPath() string {
dir, err := os.UserCacheDir()
if err != nil {
return ""
}
return filepath.Join(dir, "aego", "logs", "boot.log")
}
func StartBoot(path string, lines ...string) error {
boot.mu.Lock()
defer boot.mu.Unlock()
if boot.f != nil {
boot.f.Close()
boot.f, boot.path = nil, ""
}
if path == "" {
return nil
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
_ = os.Rename(path, strings.TrimSuffix(path, ".log")+".prev.log")
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
if err != nil {
return err
}
boot.f, boot.path = f, path
for _, l := range lines {
writeBoot("start", l)
}
return nil
}
func BootPath() string {
boot.mu.Lock()
defer boot.mu.Unlock()
return boot.path
}
func Boot(stage, detail string) {
boot.mu.Lock()
defer boot.mu.Unlock()
writeBoot(stage, detail)
}
func writeBoot(stage, detail string) {
if boot.f == nil {
return
}
boot.f.WriteString(time.Now().Format("15:04:05.000") + " " + stage + ": " + detail + "\n")
boot.f.Sync()
}
func FatalText(title, msg string) string {
text := title + " cannot start: " + msg
if p := BootPath(); p != "" {
text += "\n\nDetails: " + p
}
return text
}
func RecoverFatal(title string) {
r := recover()
if r == nil {
return
}
msg := fmt.Sprint(r)
Boot("panic", msg)
Boot("stack", string(debug.Stack()))
text := FatalText(title, msg)
if !ShowFatal(title, text) {
fmt.Fprintln(os.Stderr, text)
}
os.Exit(2)
}
+7
View File
@@ -0,0 +1,7 @@
//go:build !windows
package platform
func ShowFatal(title, msg string) bool {
return false
}
+30
View File
@@ -0,0 +1,30 @@
//go:build windows
package platform
import (
"strings"
"syscall"
"unsafe"
)
const (
mbIconError = 0x10
mbSetForeground = 0x10000
mbTopmost = 0x40000
)
var messageBox = syscall.NewLazyDLL("user32.dll").NewProc("MessageBoxW")
func ShowFatal(title, msg string) bool {
t, err := syscall.UTF16PtrFromString(strings.ReplaceAll(title, "\x00", ""))
if err != nil {
return false
}
m, err := syscall.UTF16PtrFromString(strings.ReplaceAll(msg, "\x00", ""))
if err != nil {
return false
}
r, _, _ := messageBox.Call(0, uintptr(unsafe.Pointer(m)), uintptr(unsafe.Pointer(t)), mbIconError|mbSetForeground|mbTopmost)
return r != 0
}
+33
View File
@@ -552,3 +552,36 @@ bool aego_set_icon(SDL_Window* w, SDL_Surface* s) {
SDL_DestroySurface(s);
return ok;
}
#if defined(_WIN32)
#define AEGO_GLAPI __stdcall
#else
#define AEGO_GLAPI
#endif
typedef const unsigned char*(AEGO_GLAPI* aego_getstring)(unsigned int);
const char* aego_gl_string(unsigned int name) {
aego_getstring fn = (aego_getstring)SDL_GL_GetProcAddress("glGetString");
if (!fn) {
return "";
}
const unsigned char* v = fn(name);
return v ? (const char*)v : "";
}
const char* aego_gl_probe(SDL_Window* w) {
static char buf[256];
buf[0] = 0;
SDL_GL_ResetAttributes();
SDL_GLContext c = SDL_GL_CreateContext(w);
if (!c) {
return buf;
}
if (SDL_GL_MakeCurrent(w, c)) {
SDL_snprintf(buf, sizeof buf, "%s (%s)", aego_gl_string(0x1F02), aego_gl_string(0x1F01));
}
SDL_GL_MakeCurrent(w, NULL);
SDL_GL_DestroyContext(c);
return buf;
}
+3
View File
@@ -125,4 +125,7 @@ SDL_Surface* aego_icon_surface(int w, int h, const void* rgba);
bool aego_icon_alternate(SDL_Surface* base, SDL_Surface* alt);
bool aego_set_icon(SDL_Window* w, SDL_Surface* s);
const char* aego_gl_string(unsigned int name);
const char* aego_gl_probe(SDL_Window* w);
#endif
+26 -6
View File
@@ -63,10 +63,21 @@ func setMetadata(m platform.AppMetadata) {
C.SDL_SetAppMetadata(name, ver, id)
}
func glMessage(found string) string {
if found == "" {
found = "no OpenGL at all"
}
return "OpenGL 3.3 is required, the graphics driver provides " + found +
". Install the driver from the GPU vendor; in a virtual machine or over Remote Desktop turn on 3D acceleration or run Aego on the host"
}
func New() (*Platform, error) {
setMetadata(platform.AppMeta())
platform.Boot("sdl", "init")
if !C.aego_init() {
return nil, errs.New(errs.PlatformInit, "SDL_Init failed", log.F("sdl", lastError()))
msg := lastError()
platform.Boot("sdl", "init failed: "+msg)
return nil, errs.New(errs.PlatformInit, "SDL_Init failed", log.F("sdl", msg))
}
v := C.aego_version()
p := &Platform{
@@ -82,6 +93,7 @@ func New() (*Platform, error) {
},
start: uint64(C.aego_ticks_ns()),
}
platform.Boot("sdl", "SDL "+p.info.Version+", video "+p.info.VideoDriver)
return p, nil
}
@@ -112,23 +124,30 @@ func (p *Platform) CreateWindow(desc platform.WindowDesc) (platform.Window, erro
}
title := C.CString(desc.Title)
defer C.free(unsafe.Pointer(title))
platform.Boot("window", fmt.Sprintf("%dx%d hidden=%v", desc.Width, desc.Height, desc.Hidden))
sw := C.aego_create_window(title, C.int(desc.Width), C.int(desc.Height),
boolInt(desc.Resizable), boolInt(desc.Fullscreen), boolInt(desc.HighDPI))
if sw == nil {
return nil, errs.New(errs.PlatformWindow, "SDL_CreateWindow failed", log.F("sdl", lastError()))
msg := lastError()
platform.Boot("window", "failed: "+msg)
return nil, errs.New(errs.PlatformWindow, "cannot open a window: "+msg, log.F("sdl", msg))
}
ctx := C.SDL_GL_CreateContext(sw)
if ctx == nil {
err := errs.New(errs.PlatformContext, "SDL_GL_CreateContext failed", log.F("sdl", lastError()))
msg := lastError()
found := C.GoString(C.aego_gl_probe(sw))
platform.Boot("gl", "3.3 core context failed: "+msg+"; driver has "+found)
C.SDL_DestroyWindow(sw)
return nil, err
return nil, errs.New(errs.PlatformContext, glMessage(found), log.F("sdl", msg))
}
if !C.SDL_GL_MakeCurrent(sw, ctx) {
err := errs.New(errs.PlatformContext, "SDL_GL_MakeCurrent failed", log.F("sdl", lastError()))
msg := lastError()
platform.Boot("gl", "make current failed: "+msg)
C.SDL_GL_DestroyContext(ctx)
C.SDL_DestroyWindow(sw)
return nil, err
return nil, errs.New(errs.PlatformContext, "SDL_GL_MakeCurrent failed", log.F("sdl", msg))
}
platform.Boot("gl", C.GoString(C.aego_gl_string(0x1F02))+" ("+C.GoString(C.aego_gl_string(0x1F01))+")")
w := &Window{p: p, win: sw, ctx: ctx, fullscreen: desc.Fullscreen, cursor: 255}
w.SetVSync(desc.VSync)
C.SDL_StopTextInput(sw)
@@ -138,6 +157,7 @@ func (p *Platform) CreateWindow(desc platform.WindowDesc) (platform.Window, erro
}
w.refresh()
p.win = w
platform.Boot("window", "shown")
return w, nil
}