Files
aego/gpu/gl33/device.go
T
sanspie 69d19e501f Stage 7: documentation, help, playground; templates, aego new/bootstrap/icons, launcher
Documentation: engine/docs assembles one registry from the script
bindings, the schema, module docs and module-owned script paths; problems
are reported as warnings at build time. Module registries record doc
sources and script module owners; diag.Codes lists every diagnostic code.

Project bootstrap: project.defaultSpace ("2d"/"3d"), EnsureDirs creates
the default folders when the editor opens a project (reported in the
console), prefabs/ is a pipeline root. Native builds drop -trimpath so
binding locations resolve inside native/.

Editor files: create scene/prefab/script from menus, move/rename/duplicate
/delete with undo, script package retargeting on move, open scenes and
script panels follow renames, Save As / Rename Scene prompts, scene
picker, startup scene. Scoped shortcuts fall back to the global command
when disabled; Save As is Ctrl+Shift+S, Save All Ctrl+Alt+S.

Script editor: Go to Implementation (stub → native/ source), host
definitions always resolve to stubs, bracket match/mismatch states,
rainbow brackets (persisted), mono code font (ui-mono atlases, per-command
fonts in draw.List, PushFont/PopFont restored by Mark/Restore).

Tests: langsvc definition/implementation, bracket states, dispatch
fallback, script renames, font stack, editor file-op integration, golden
mono glyph and bracket references; depcheck rule for engine/docs.

Docs pipeline: engine, render2d and script/engine ship their own res/docs;
keyword hover text comes from the registry through manifest.Keywords;
project script symbols join the registry; aego docs export|check|build|
serve, --tool docs in native binaries, aego check --explain; app/docstool
and the tools/gendocs module (goldmark) build the static site.

Playground: app/playground + cmd/aego-playground (js/wasm, 3.3 MB gzip)
compile, highlight and run scripts in the browser; run blocks in the
language docs become examples. engine/builtin/headless is the module set
without render3d; workspace.Config.FS; StubFor/StubPaths; backends and the
script importer drop out of js builds.

VM: one frame pointer in the interpreter loop, direct host calls, panic
recovery once per run instead of per host call (Update2k -19%,
Schedule10k -24% against the previous head). bench-build/compare/profile
run pre-built, pinned, interleaved binaries.

Editor: structural bracket levels and current-block highlight, indent
guides, occurrence highlighting with write marks, multi-format clipboard
(copy/cut/paste assets, paste images as sprites or textures). gl33 probes
a renderable depth format and reports framebuffer failures by stage.

7 E:
- project templates empty/2d/2.5d/3d, app/projects as the single creation path (D148)
- launcher and recent projects (D149)
- orthographic Camera3D and Scene View (D150)
- reference states in the Inspector, ids.Short (D151)
- sceneio remaps node references when loading without KeepIDs (D152)
- exported ComponentRef<T> (D153)
- aego icons via module.Audit (D146)
2026-10-13 01:08:56 +03:00

353 lines
8.8 KiB
Go

package gl33
import (
"strings"
"unsafe"
"aego/core/errs"
"aego/core/handle"
"aego/core/log"
"aego/gpu"
"aego/gpu/gl33/gl"
"aego/gpu/gl33/internal/glstate"
)
const scratchUnit = glstate.MaxTextureUnits - 1
type Options struct {
Debug bool
Log *log.Logger
}
type glBuffer struct {
id uint32
usage gpu.BufferUsage
size int
index gpu.IndexFormat
}
type glTexture struct {
id uint32
w, h int
info texInfo
mips bool
}
type glSampler struct {
id uint32
}
type glShader struct {
program uint32
}
const _ = uint(glstate.MaxVertexSlots - gpu.MaxVertexSlots)
const _ = uint(gpu.MaxVertexSlots - glstate.MaxVertexSlots)
type glPipeline struct {
vao uint32
program uint32
prim gpu.Primitive
mode uint32
attrs []glstate.Attrib
used [gpu.MaxVertexSlots]bool
bind glstate.VertexBinding
blend glstate.Blend
depth glstate.Depth
cull glstate.Cull
}
type resourceKind uint8
const (
kindBuffer resourceKind = iota
kindTexture
kindSampler
kindShader
kindPipeline
kindQuery
)
type pendingDestroy struct {
kind resourceKind
index, gen uint32
}
type Device struct {
log *log.ChannelLogger
debug bool
caps gpu.Capabilities
api realAPI
cache *glstate.Cache
buffers *handle.Arena[glBuffer]
textures *handle.Arena[glTexture]
samplers *handle.Arena[glSampler]
shaders *handle.Arena[glShader]
pipelines *handle.Arena[glPipeline]
queries *handle.Arena[glQuery]
fbos map[fboKey]uint32
frame frame
pass pass
inFrame bool
activeQuery uint32
pending []pendingDestroy
fbW, fbH int
debugProc gl.DebugProc
depth texInfo
}
func New(getProc func(string) unsafe.Pointer, opts Options) (*Device, error) {
if err := gl.InitWithProcAddrFunc(getProc); err != nil {
return nil, errs.Wrap(err, errs.GPUInit, "load OpenGL 3.3 core functions")
}
logger := opts.Log
if logger == nil {
logger = log.Nop()
}
d := &Device{
log: logger.With(log.GPU),
debug: opts.Debug,
api: realAPI{},
cache: glstate.New(realAPI{}),
buffers: newArena[glBuffer](logger, "buffer", 256),
textures: newArena[glTexture](logger, "texture", 128),
samplers: newArena[glSampler](logger, "sampler", 16),
shaders: newArena[glShader](logger, "shader", 32),
pipelines: newArena[glPipeline](logger, "pipeline", 32),
queries: newArena[glQuery](logger, "query", 16),
fbos: make(map[fboKey]uint32, 16),
pending: make([]pendingDestroy, 0, 64),
}
d.frame.d = d
d.pass.d = d
d.queryCaps()
d.depth = d.probeDepth()
if d.debug && d.caps.DebugOutput {
d.installDebugOutput()
}
gl.Enable(gl.FRAMEBUFFER_SRGB)
gl.PixelStorei(gl.UNPACK_ALIGNMENT, 1)
return d, nil
}
func newArena[T any](l *log.Logger, kind string, capacity int) *handle.Arena[T] {
a := handle.NewArena[T](capacity)
cl := l.With(log.GPU)
a.OnGrow = func(from, to int) {
cl.Warn("resource arena grows", log.F("kind", kind), log.F("from", from), log.F("to", to))
}
return a
}
func (d *Device) Caps() gpu.Capabilities {
return d.caps
}
func (d *Device) queryCaps() {
c := &d.caps
c.Vendor = gl.GoStr(gl.GetString(gl.VENDOR))
c.Renderer = gl.GoStr(gl.GetString(gl.RENDERER))
c.Version = gl.GoStr(gl.GetString(gl.VERSION))
c.GLSL = gl.GoStr(gl.GetString(gl.SHADING_LANGUAGE_VERSION))
c.MaxTextureSize = getInt(gl.MAX_TEXTURE_SIZE)
c.MaxTextureUnits = min(getInt(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS), scratchUnit)
c.MaxSamples = getInt(gl.MAX_SAMPLES)
c.Instancing = true
c.TimerQueries = true
c.TextureArrays = true
major, minor := getInt(gl.MAJOR_VERSION), getInt(gl.MINOR_VERSION)
exts := extensions()
c.DebugOutput = exts["GL_KHR_debug"] || major > 4 || (major == 4 && minor >= 3)
c.SRGB = defaultFramebufferSRGB()
c.ClipZeroToOne = false
c.OriginBottomLeft = true
}
func getInt(pname uint32) int {
var v int32
gl.GetIntegerv(pname, &v)
return int(v)
}
func extensions() map[string]bool {
n := getInt(gl.NUM_EXTENSIONS)
out := make(map[string]bool, n)
for i := 0; i < n; i++ {
out[gl.GoStr(gl.GetStringi(gl.EXTENSIONS, uint32(i)))] = true
}
return out
}
func defaultFramebufferSRGB() bool {
var enc int32
gl.BindFramebuffer(gl.FRAMEBUFFER, 0)
gl.GetFramebufferAttachmentParameteriv(gl.FRAMEBUFFER, gl.BACK_LEFT, gl.FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING, &enc)
if gl.GetError() != gl.NO_ERROR {
gl.GetFramebufferAttachmentParameteriv(gl.FRAMEBUFFER, gl.BACK, gl.FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING, &enc)
for gl.GetError() != gl.NO_ERROR {
}
}
return enc == gl.SRGB
}
func (d *Device) installDebugOutput() {
logger := d.log.Logger()
d.debugProc = func(source, gltype, id, severity uint32, length int32, message string, userParam unsafe.Pointer) {
lv := log.Debug
switch severity {
case gl.DEBUG_SEVERITY_HIGH:
lv = log.Error
case gl.DEBUG_SEVERITY_MEDIUM:
lv = log.Warn
case gl.DEBUG_SEVERITY_LOW:
lv = log.Info
}
if !logger.Enabled(lv) {
return
}
logger.Log(log.GPU, lv, strings.TrimSpace(message),
log.F("id", id),
log.F("type", debugTypeName(gltype)),
log.F("source", debugSourceName(source)))
}
gl.Enable(gl.DEBUG_OUTPUT)
gl.Enable(gl.DEBUG_OUTPUT_SYNCHRONOUS)
gl.DebugMessageCallback(d.debugProc, nil)
gl.DebugMessageControl(gl.DONT_CARE, gl.DONT_CARE, gl.DEBUG_SEVERITY_NOTIFICATION, 0, nil, false)
d.log.Debug("KHR_debug output enabled")
}
func (d *Device) label(kind, id uint32, label string) {
if !d.debug || !d.caps.DebugOutput || label == "" {
return
}
cs, free := gl.Strs(label + "\x00")
gl.ObjectLabel(kind, id, int32(len(label)), *cs)
free()
}
func (d *Device) checkError(op, label string) error {
if !d.debug {
return nil
}
var codes []string
for i := 0; i < 8; i++ {
e := gl.GetError()
if e == gl.NO_ERROR {
break
}
codes = append(codes, glErrorName(e))
}
if len(codes) == 0 {
return nil
}
return errs.New(errs.GPUResource, op+" failed",
log.F("label", label), log.F("gl", strings.Join(codes, ",")))
}
func (d *Device) BeginFrame(pixelW, pixelH int) gpu.Frame {
d.fbW, d.fbH = pixelW, pixelH
d.frame.stats = gpu.FrameStats{}
d.inFrame = true
return &d.frame
}
func (d *Device) EndFrame() {
if d.pass.active {
d.log.Warn("render pass was not ended before EndFrame")
d.pass.active = false
}
d.inFrame = false
for _, p := range d.pending {
d.destroyNow(p.kind, p.index, p.gen)
}
d.pending = d.pending[:0]
}
func (d *Device) Close() {
d.pending = d.pending[:0]
d.inFrame = false
d.cache.BindVertexArray(0)
d.cache.UseProgram(0)
d.cache.BindFramebuffer(0)
d.deleteFramebuffers()
d.pipelines.Each(func(_ handle.Handle[glPipeline], p *glPipeline) {
id := p.vao
gl.DeleteVertexArrays(1, &id)
})
d.shaders.Each(func(_ handle.Handle[glShader], s *glShader) {
gl.DeleteProgram(s.program)
})
d.samplers.Each(func(_ handle.Handle[glSampler], s *glSampler) {
id := s.id
gl.DeleteSamplers(1, &id)
})
d.textures.Each(func(_ handle.Handle[glTexture], t *glTexture) {
id := t.id
gl.DeleteTextures(1, &id)
})
d.buffers.Each(func(_ handle.Handle[glBuffer], b *glBuffer) {
id := b.id
gl.DeleteBuffers(1, &id)
})
d.queries.Each(func(_ handle.Handle[glQuery], q *glQuery) {
id := q.id
gl.DeleteQueries(1, &id)
})
d.pipelines.Clear()
d.shaders.Clear()
d.samplers.Clear()
d.textures.Clear()
d.buffers.Clear()
d.queries.Clear()
d.activeQuery = 0
d.cache.Invalidate()
}
func (d *Device) formatInfo(f gpu.TextureFormat) (texInfo, bool) {
if f == gpu.TexDepth24Stencil8 && d.depth.internal != 0 {
return d.depth, true
}
return textureInfo(f)
}
func (d *Device) probeDepth() texInfo {
for i, cand := range depthFormats {
if !d.depthWorks(cand) {
continue
}
if i > 0 {
d.log.Warn("depth-stencil render targets are incomplete on this driver, using a depth-only format",
log.F("format", cand.name), log.F("renderer", d.caps.Renderer))
}
return cand
}
d.log.Error("no depth format is renderable, 3D views will fail to create render targets", log.F("renderer", d.caps.Renderer))
return depthFormats[0]
}
func (d *Device) depthWorks(info texInfo) bool {
for gl.GetError() != gl.NO_ERROR {
}
var tex [2]uint32
gl.GenTextures(2, &tex[0])
d.bindScratch(tex[0])
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, 4, 4, 0, gl.RGBA, gl.UNSIGNED_BYTE, nil)
d.bindScratch(tex[1])
gl.TexImage2D(gl.TEXTURE_2D, 0, info.internal, 4, 4, 0, info.format, info.xtype, nil)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
var fbo uint32
gl.GenFramebuffers(1, &fbo)
gl.BindFramebuffer(gl.FRAMEBUFFER, fbo)
gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex[0], 0)
gl.FramebufferTexture2D(gl.FRAMEBUFFER, info.attach, gl.TEXTURE_2D, tex[1], 0)
ok := gl.CheckFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE && gl.GetError() == gl.NO_ERROR
gl.BindFramebuffer(gl.FRAMEBUFFER, 0)
gl.DeleteFramebuffers(1, &fbo)
d.cache.ForgetTexture(tex[0])
d.cache.ForgetTexture(tex[1])
gl.DeleteTextures(2, &tex[0])
d.cache.Invalidate()
return ok
}