ci-platforms / windows amd64 smoke (push) Skipped
ci / release (push) Successful in 8m58s
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / checks (push) Successful in 4m46s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 13m28s
498 lines
17 KiB
Go
498 lines
17 KiB
Go
//go:build js && wasm
|
|
|
|
package main
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"embed"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"syscall/js"
|
|
"time"
|
|
|
|
"aego/asset"
|
|
"aego/core/ids"
|
|
"aego/core/log"
|
|
"aego/core/mathx"
|
|
"aego/engine/res"
|
|
"aego/gpu"
|
|
"aego/gpu/webgl2"
|
|
"aego/render"
|
|
"aego/render/material"
|
|
"aego/render/mesh"
|
|
"aego/render/shader"
|
|
"aego/render/sprite"
|
|
"aego/render/texture"
|
|
"aego/render/view"
|
|
"aego/tests/materialgolden"
|
|
"aego/vfs"
|
|
)
|
|
|
|
//go:embed fixtures/*.aesh
|
|
var fixtures embed.FS
|
|
|
|
type report struct {
|
|
Passed []string `json:"passed"`
|
|
Failures []string `json:"failures"`
|
|
Renderer string `json:"renderer"`
|
|
Parallel bool `json:"parallelShaderCompile"`
|
|
Restores int `json:"contextRestores"`
|
|
Images map[string]string `json:"imageSHA256"`
|
|
PNG map[string]string `json:"imagePNG"`
|
|
}
|
|
|
|
func (r *report) check(name string, ok bool, detail any) {
|
|
if ok {
|
|
r.Passed = append(r.Passed, name)
|
|
} else {
|
|
r.Failures = append(r.Failures, fmt.Sprintf("%s: %v", name, detail))
|
|
}
|
|
}
|
|
func (r *report) image(name string, pixels []byte) {
|
|
h := sha256.Sum256(pixels)
|
|
r.Images[name] = hex.EncodeToString(h[:])
|
|
if _, ok := materialgolden.Reference(name); ok {
|
|
err := materialgolden.Compare(name, pixels, 2)
|
|
r.check("golden-"+name, err == nil, err)
|
|
}
|
|
canvas := js.Global().Get("document").Call("createElement", "canvas")
|
|
canvas.Set("width", 32)
|
|
canvas.Set("height", 32)
|
|
ctx := canvas.Call("getContext", "2d")
|
|
im := ctx.Call("createImageData", 32, 32)
|
|
js.CopyBytesToJS(im.Get("data"), pixels)
|
|
ctx.Call("putImageData", im, 0, 0)
|
|
r.PNG[name] = canvas.Call("toDataURL", "image/png").String()
|
|
}
|
|
|
|
type surface struct{ ctx js.Value }
|
|
|
|
func (s surface) WebGL2() js.Value { return s.ctx }
|
|
|
|
// The completion gate places context loss between real WebGL compile/link submission and cache consumption, even on a fast driver; driver, context events and recreation are not mocked.
|
|
type device struct {
|
|
*webgl2.Device
|
|
hold bool
|
|
submitted int
|
|
}
|
|
type job struct {
|
|
gpu.ShaderCompilation
|
|
d *device
|
|
}
|
|
|
|
func (j job) Poll() (gpu.ShaderHandle, bool, error) {
|
|
if j.d.hold {
|
|
return gpu.ShaderHandle{}, false, nil
|
|
}
|
|
return j.ShaderCompilation.Poll()
|
|
}
|
|
func (d *device) BeginShaderCompile(desc gpu.ShaderDesc) (gpu.ShaderCompilation, error) {
|
|
inner, err := d.Device.BeginShaderCompile(desc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
d.submitted++
|
|
return job{inner, d}, nil
|
|
}
|
|
|
|
type rig struct {
|
|
r *render.Renderer
|
|
d *device
|
|
target gpu.TextureHandle
|
|
region texture.Region
|
|
white texture.Region
|
|
pixels []byte
|
|
errors []string
|
|
tex map[ids.AssetID]material.Tex
|
|
}
|
|
|
|
func newRig(ctx js.Value) (*rig, error) {
|
|
d, err := webgl2.New(surface{ctx}, webgl2.Options{Debug: true, Log: log.Nop()})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
wrapped := &device{Device: d}
|
|
files, err := vfs.New(vfs.Mount{Scheme: "engine", Source: res.Source(), ReadOnly: true})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r, err := render.New(wrapped, d.Caps(), files, log.Nop(), render.Options{MaxSprites: 64})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
target, err := d.CreateTexture(gpu.TextureDesc{Width: 32, Height: 32, Format: gpu.TexRGBA8, Label: "webmaterials.target"})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
g := &rig{r: r, d: wrapped, target: target, white: r.White(), pixels: make([]byte, 32*32*4), tex: map[ids.AssetID]material.Tex{}}
|
|
r.Materials().OnError = func(sh *material.Shader, v int, err error) {
|
|
g.errors = append(g.errors, fmt.Sprintf("%s/%d: %v", sh.Name, v, err))
|
|
}
|
|
data := texture.Solid(4, 4, mathx.Color{R: 1, A: 0.5})
|
|
data.SetPixel(0, 0, mathx.Color{})
|
|
h, err := r.Textures().Upload(data, texture.UploadOptions{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
g.region = texture.Whole(h, 4, 4)
|
|
return g, nil
|
|
}
|
|
|
|
func (g *rig) builtin(name string) *material.Material {
|
|
mid, ok := asset.BuiltinMaterialNamed(name)
|
|
if !ok {
|
|
panic("unknown builtin " + name)
|
|
}
|
|
data, ok := asset.BuiltinMaterial(mid)
|
|
if !ok {
|
|
panic("missing builtin artifact")
|
|
}
|
|
doc, err := asset.DecodeMaterial(data, name)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
src, ok := asset.BuiltinShader(doc.Shader)
|
|
if !ok {
|
|
panic("missing builtin shader")
|
|
}
|
|
a, err := asset.DecodeShader(src)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
sh := a.Runtime(doc.Shader, name)
|
|
m, probs := doc.Build(name, sh, func(id ids.AssetID) (material.Tex, bool) {
|
|
if t, ok := g.tex[id]; ok {
|
|
return t, true
|
|
}
|
|
d, ok := asset.BuiltinTexture(id)
|
|
if !ok {
|
|
return material.Tex{}, false
|
|
}
|
|
h, err := g.r.Textures().Upload(d, texture.UploadOptions{})
|
|
if err != nil {
|
|
return material.Tex{}, false
|
|
}
|
|
t := material.Tex{Texture: h}
|
|
g.tex[id] = t
|
|
return t, true
|
|
})
|
|
if m == nil {
|
|
panic(probs)
|
|
}
|
|
m.ID = mid
|
|
return m
|
|
}
|
|
|
|
func (g *rig) shader(name string) *material.Shader {
|
|
data, err := fixtures.ReadFile("fixtures/" + name + ".aesh")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
a, err := asset.DecodeShader(data)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return a.Runtime(ids.AssetID{}, name)
|
|
}
|
|
func (g *rig) material(name string) *material.Material {
|
|
m, ok := material.New(name, g.shader(name), 0, shader.BlendPremultiplied)
|
|
if !ok {
|
|
panic("missing variant")
|
|
}
|
|
return m
|
|
}
|
|
func (g *rig) draw(v *render.ViewFrame, m *material.Material, lanes *[16]float32) {
|
|
v.Sprite(sprite.Draw{Region: g.region, Position: mathx.Vec2{X: 16, Y: 16}, Size: mathx.Vec2{X: 24, Y: 24}, Origin: mathx.Vec2{X: .5, Y: .5}, Tint: mathx.White, Material: m, Lanes: lanes})
|
|
}
|
|
func (g *rig) checker(v *render.ViewFrame) {
|
|
for y := 0; y < 4; y++ {
|
|
for x := 0; x < 4; x++ {
|
|
c := float32(.25)
|
|
if (x+y)%2 != 0 {
|
|
c = .75
|
|
}
|
|
v.Sprite(sprite.Draw{Region: g.white, Position: mathx.Vec2{X: float32(x * 8), Y: float32(y * 8)}, Size: mathx.Vec2{X: 8, Y: 8}, Tint: mathx.Color{R: c, G: c, B: c, A: 1}})
|
|
}
|
|
}
|
|
}
|
|
func (g *rig) frame(draw func(*render.ViewFrame)) []byte {
|
|
f := g.d.BeginFrame(32, 32)
|
|
if err := g.r.BeginFrame(f, 32, 32, 1); err != nil {
|
|
panic(err)
|
|
}
|
|
v := view.ScreenView(32, 32, 1)
|
|
v.Target = g.target
|
|
v.DoClear = true
|
|
vf := g.r.View(v)
|
|
draw(vf)
|
|
vf.End()
|
|
g.r.EndFrame()
|
|
g.d.EndFrame()
|
|
if !g.d.ContextLost() {
|
|
if err := g.d.ReadPixels(g.target, 0, 0, 32, 32, g.pixels); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
return g.pixels
|
|
}
|
|
func pixel(b []byte, x, y int) [4]byte { return [4]byte(b[(y*32+x)*4 : (y*32+x)*4+4]) }
|
|
func near(v [4]byte, w [4]int) bool {
|
|
for i, n := range w {
|
|
if int(v[i]) < n-2 || int(v[i]) > n+2 {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
func wait(test func() bool, what string) {
|
|
until := time.Now().Add(15 * time.Second)
|
|
for !test() {
|
|
if time.Now().After(until) {
|
|
panic("timeout: " + what)
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
// Go may resume with the js.FuncOf lost-event callback on the JS stack; a new JS task, not a longer poll, ensures dispatch finishes
|
|
// and preventDefault is observed before restoreContext.
|
|
func afterBrowserEvent() {
|
|
done := make(chan struct{})
|
|
callback := js.FuncOf(func(js.Value, []js.Value) any {
|
|
close(done)
|
|
return nil
|
|
})
|
|
defer callback.Release()
|
|
js.Global().Call("setTimeout", callback, 0)
|
|
<-done
|
|
}
|
|
func (g *rig) commit() {
|
|
wait(func() bool { g.frame(func(*render.ViewFrame) {}); return g.r.Materials().Stats().Pending == 0 }, "material transaction")
|
|
}
|
|
|
|
func run(out *report) {
|
|
canvas := js.Global().Get("document").Call("getElementById", "surface")
|
|
ctx := canvas.Call("getContext", "webgl2", map[string]any{"alpha": true, "premultipliedAlpha": true, "antialias": false, "preserveDrawingBuffer": true})
|
|
if !ctx.Truthy() {
|
|
panic("WebGL2 unavailable")
|
|
}
|
|
out.Parallel = ctx.Call("getExtension", "KHR_parallel_shader_compile").Truthy()
|
|
g, err := newRig(ctx)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer g.d.Close()
|
|
defer g.r.Close()
|
|
out.Renderer = g.d.Caps().Renderer
|
|
builtins := map[string]*material.Material{}
|
|
for _, name := range materialgolden.Stage12aBuiltins {
|
|
m := g.builtin(name)
|
|
builtins[name] = m
|
|
img := g.frame(func(v *render.ViewFrame) { g.draw(v, m, nil) })
|
|
out.check("builtin-"+name+"-links", len(g.errors) == 0, g.errors)
|
|
out.check("builtin-"+name+"-transparent-hole", pixel(img, 6, 6) == [4]byte{}, pixel(img, 6, 6))
|
|
out.image("builtin-"+name, img)
|
|
}
|
|
unlit := builtins["Unlit"]
|
|
unlit.Set("Opacity", [4]float32{.5})
|
|
img := g.frame(func(v *render.ViewFrame) { g.draw(v, unlit, nil) })
|
|
out.check("premultiplied-opacity-once", near(pixel(img, 16, 16), [4]int{64, 0, 0, 64}), pixel(img, 16, 16))
|
|
out.image("opacity", img)
|
|
unlit.Reset("Opacity")
|
|
img = g.frame(func(v *render.ViewFrame) { g.checker(v); g.draw(v, unlit, nil) })
|
|
out.check("unlit-over-checkerboard", near(pixel(img, 16, 16), [4]int{160, 32, 32, 255}), pixel(img, 16, 16))
|
|
out.image("checker-unlit", img)
|
|
flash := builtins["Flash"]
|
|
flash.Set("Flash", [4]float32{1})
|
|
img = g.frame(func(v *render.ViewFrame) { g.draw(v, flash, nil) })
|
|
out.check("flash-preserves-coverage", near(pixel(img, 16, 16), [4]int{128, 128, 128, 128}) && pixel(img, 6, 6) == [4]byte{}, pixel(img, 16, 16))
|
|
out.image("flash", img)
|
|
dissolve := builtins["Dissolve"]
|
|
dissolve.Set("Threshold", [4]float32{1})
|
|
img = g.frame(func(v *render.ViewFrame) { g.draw(v, dissolve, nil) })
|
|
allZero := true
|
|
for _, b := range img {
|
|
if b != 0 {
|
|
allZero = false
|
|
break
|
|
}
|
|
}
|
|
out.check("dissolve-one-is-fully-transparent", allZero, pixel(img, 16, 16))
|
|
out.image("dissolved", img)
|
|
dissolve.Reset("Threshold")
|
|
img = g.frame(func(v *render.ViewFrame) { g.draw(v, dissolve, nil) })
|
|
out.check("dissolve-zero-is-original", near(pixel(img, 16, 16), [4]int{128, 0, 0, 128}), pixel(img, 16, 16))
|
|
add := builtins["Additive"]
|
|
img = g.frame(func(v *render.ViewFrame) { g.draw(v, add, nil); g.draw(v, add, nil) })
|
|
out.check("additive-preserves-background-coverage", near(pixel(img, 16, 16), [4]int{255, 0, 0, 0}), pixel(img, 16, 16))
|
|
out.image("additive", img)
|
|
outline := builtins["Outline"]
|
|
img = g.frame(func(v *render.ViewFrame) { g.draw(v, outline, nil) })
|
|
out.check("outline-is-interior", near(pixel(img, 6, 16), [4]int{0, 0, 0, 128}) && pixel(img, 3, 16) == [4]byte{}, pixel(img, 6, 16))
|
|
typed := g.material("typed")
|
|
lanes := *typed.DefaultLanes()
|
|
p, _ := typed.Shader().Schema.Param("Code")
|
|
material.PutLaneInt(&lanes, p, 16777217)
|
|
p, _ = typed.Shader().Schema.Param("Enabled")
|
|
material.PutLane(&lanes, p, [4]float32{1})
|
|
p, _ = typed.Shader().Schema.Param("Amount")
|
|
material.PutLane(&lanes, p, [4]float32{.5})
|
|
img = g.frame(func(v *render.ViewFrame) {
|
|
g.draw(v, typed, &lanes)
|
|
v.Sprite(sprite.Draw{Region: g.region, Position: mathx.Vec2{X: 1, Y: 1}, Size: mathx.Vec2{X: 1, Y: 1}, Tint: mathx.White, Material: typed})
|
|
})
|
|
out.check("typed-int32-bool-and-std140", near(pixel(img, 16, 16), [4]int{0, 128, 0, 128}), pixel(img, 16, 16))
|
|
out.check("overrides-do-not-split-batch", g.r.Stats().Batches == 1, g.r.Stats())
|
|
geometryData := &mesh.Data{Vertices: []mesh.Vertex{{Pos: mathx.Vec3{X: 4, Y: 4}, Color: mesh.White}, {Pos: mathx.Vec3{X: 28, Y: 4}, Color: mesh.White}, {Pos: mathx.Vec3{X: 28, Y: 28}, Color: mesh.White}, {Pos: mathx.Vec3{X: 4, Y: 28}, Color: mesh.White}}, Indices: []uint32{0, 1, 2, 0, 2, 3}}
|
|
geometryData.ComputeBounds()
|
|
geometry, e := mesh.Upload(g.d, geometryData, "web-mesh")
|
|
if e != nil {
|
|
panic(e)
|
|
}
|
|
defer geometry.Destroy(g.d)
|
|
meshMaterial := g.material("mesh-typed")
|
|
meshLanes := *meshMaterial.DefaultLanes()
|
|
mp, _ := meshMaterial.Shader().Schema.Param("Code")
|
|
material.PutLaneInt(&meshLanes, mp, 16777217)
|
|
img = g.frame(func(v *render.ViewFrame) {
|
|
v.Mesh(mesh.Draw{Mesh: geometry, World: mathx.Mat4Identity(), Is2D: true, Material: meshMaterial, Color: mathx.White, Lanes: &meshLanes})
|
|
})
|
|
out.check("mesh2d-typed-override", near(pixel(img, 16, 16), [4]int{0, 128, 0, 128}), pixel(img, 16, 16))
|
|
red := g.material("reload-red")
|
|
img = g.frame(func(v *render.ViewFrame) {
|
|
g.draw(v, red, nil)
|
|
v.Mesh(mesh.Draw{Mesh: geometry, World: mathx.Mat4Identity(), Is2D: true, Material: meshMaterial, Color: mathx.White, Lanes: &meshLanes})
|
|
g.draw(v, red, nil)
|
|
})
|
|
out.check("sprite-mesh-sprite-painter-order", near(pixel(img, 16, 16), [4]int{159, 64, 0, 223}), pixel(img, 16, 16))
|
|
frame := g.d.BeginFrame(32, 32)
|
|
if e := g.r.BeginFrame(frame, 32, 32, 1); e != nil {
|
|
panic(e)
|
|
}
|
|
camera := view.ScreenView(32, 32, 1)
|
|
camera.Target = g.target
|
|
camera.DoClear = true
|
|
camera.Clear = mathx.Color{R: 1, A: 1}
|
|
g.r.View(camera).End()
|
|
camera.Viewport = mathx.Rect{X: 20, Y: 20, W: 10, H: 10}
|
|
camera.Clear = mathx.Color{G: 1, A: 1}
|
|
g.r.View(camera).End()
|
|
g.r.EndFrame()
|
|
g.d.EndFrame()
|
|
if e := g.d.ReadPixels(g.target, 0, 0, 32, 32, g.pixels); e != nil {
|
|
panic(e)
|
|
}
|
|
out.check("camera-clear-preserves-other-viewport", near(pixel(g.pixels, 3, 3), [4]int{255, 0, 0, 255}) && near(pixel(g.pixels, 25, 25), [4]int{0, 255, 0, 255}), pixel(g.pixels, 3, 3))
|
|
trimMaterial := g.material("trim")
|
|
trimLanes := *trimMaterial.DefaultLanes()
|
|
tp, _ := trimMaterial.Shader().Schema.Param("Code")
|
|
material.PutLaneInt(&trimLanes, tp, 16777217)
|
|
trimData := texture.Solid(5, 4, mathx.Color{G: 1, A: 1})
|
|
for y := 1; y < 3; y++ {
|
|
for x := 1; x < 4; x++ {
|
|
trimData.SetPixel(x, y, mathx.White)
|
|
}
|
|
}
|
|
trimTexture, err := g.r.Textures().Upload(trimData, texture.UploadOptions{})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
trimRegion := texture.Whole(trimTexture, 5, 4).Sub(1, 1, 3, 2)
|
|
trimRegion.SourceW, trimRegion.SourceH, trimRegion.TrimX, trimRegion.TrimY = 8, 6, 2, 1
|
|
for i, name := range []string{"trim-none", "trim-x", "trim-y", "trim-xy"} {
|
|
img = g.frame(func(v *render.ViewFrame) {
|
|
v.Sprite(sprite.Draw{Region: trimRegion, Position: mathx.Vec2{X: 16, Y: 16}, Size: mathx.Vec2{X: 16, Y: 12}, Origin: mathx.Vec2{X: .5, Y: .5}, Tint: mathx.White, Material: trimMaterial, Lanes: &trimLanes, FlipX: i&1 != 0, FlipY: i&2 != 0})
|
|
})
|
|
out.image(name, img)
|
|
}
|
|
m := g.material("reload-red")
|
|
m.Set("Amount", [4]float32{.25})
|
|
draw := func(v *render.ViewFrame) { g.draw(v, m, nil) }
|
|
img = g.frame(draw)
|
|
before := pixel(img, 16, 16)
|
|
old := m.Shader()
|
|
g.r.Materials().QueueReload(old, g.shader("broken"), []*material.Material{m}, nil)
|
|
g.commit()
|
|
img = g.frame(draw)
|
|
out.check("broken-driver-reload-keeps-last-good", m.Shader() == old && m.Failed() && pixel(img, 16, 16) == before, pixel(img, 16, 16))
|
|
g.errors = nil
|
|
next := g.shader("reload-green")
|
|
g.d.hold = true
|
|
g.r.Materials().QueueReload(old, next, []*material.Material{m}, nil)
|
|
g.frame(draw)
|
|
out.check("real-compile-awaits-commit", g.r.Materials().Stats().Pending == 1 && g.d.ResourceStats().PendingCompiles > 0, g.d.ResourceStats())
|
|
if !g.d.SimulateLoss(true) {
|
|
panic("WEBGL_lose_context unavailable")
|
|
}
|
|
wait(g.d.ContextLost, "webglcontextlost event")
|
|
afterBrowserEvent()
|
|
out.check("loss-keeps-cpu-last-good", m.Shader() == old, m.Shader().Name)
|
|
// Poll the cache while lost so it observes cancellation of the old device.
|
|
g.r.Materials().Begin()
|
|
g.r.Materials().End()
|
|
if !g.d.SimulateLoss(false) {
|
|
panic("context restoration unavailable")
|
|
}
|
|
wait(func() bool { return !g.d.ContextLost() && g.d.Restores() > 0 }, "webglcontextrestored event")
|
|
g.d.hold = false
|
|
g.commit()
|
|
img = g.frame(draw)
|
|
out.check("restored-reload-commits-new-device", m.Shader() == next && !m.Failed() && near(pixel(img, 16, 16), [4]int{0, 64, 0, 64}), pixel(img, 16, 16))
|
|
out.Restores = g.d.Restores()
|
|
base := g.d.ResourceStats()
|
|
for i := 0; i < 12; i++ {
|
|
candidate := g.shader("reload-green")
|
|
g.r.Materials().QueueReload(m.Shader(), candidate, []*material.Material{m}, nil)
|
|
g.commit()
|
|
g.frame(draw)
|
|
}
|
|
counts := g.d.ResourceStats()
|
|
out.check("repeated-reload-releases-programs", counts.Shaders == base.Shaders && counts.Pipelines == base.Pipelines && counts.Buffers == base.Buffers && counts.PendingCompiles == 0, map[string]any{"before": base, "after": counts})
|
|
img = g.frame(func(v *render.ViewFrame) { g.checker(v); g.draw(v, flash, nil) })
|
|
out.check("restored-textures-and-lanes", near(pixel(img, 16, 16), [4]int{160, 160, 160, 255}), pixel(img, 16, 16))
|
|
out.image("checker-flash-restored", img)
|
|
img = g.frame(func(v *render.ViewFrame) {
|
|
v.Sprite(sprite.Draw{Region: trimRegion, Position: mathx.Vec2{X: 16, Y: 16}, Size: mathx.Vec2{X: 16, Y: 12}, Origin: mathx.Vec2{X: .5, Y: .5}, Tint: mathx.White, Material: trimMaterial, Lanes: &trimLanes, FlipX: true, FlipY: true})
|
|
})
|
|
out.image("trim-xy-restored", img)
|
|
pixelSnap := builtins["PixelSnap"]
|
|
pixelSnap.Set("GridPixels", [4]float32{4})
|
|
pf := g.d.BeginFrame(32, 32)
|
|
if e := g.r.BeginFrame(pf, 32, 32, 2); e != nil {
|
|
panic(e)
|
|
}
|
|
pv := view.ScreenView(32, 32, 2)
|
|
pv.Target = g.target
|
|
pv.DoClear = true
|
|
g.r.View(pv).End()
|
|
pv.Viewport = mathx.Rect{X: 3, Y: 5, W: 24, H: 22}
|
|
pv.DoClear = false
|
|
pvf := g.r.View(pv)
|
|
pvf.Sprite(sprite.Draw{Region: g.white, Position: mathx.Vec2{X: 5.2, Y: 6.2}, Size: mathx.Vec2{X: 4.6, Y: 4.6}, Tint: mathx.White, Material: pixelSnap})
|
|
pvf.End()
|
|
g.r.EndFrame()
|
|
g.d.EndFrame()
|
|
if e := g.d.ReadPixels(g.target, 0, 0, 32, 32, g.pixels); e != nil {
|
|
panic(e)
|
|
}
|
|
out.image("pixelsnap-dpr2-offset", g.pixels)
|
|
g.contractChecks(out)
|
|
out.check("no-unexpected-driver-errors", len(g.errors) == 0, g.errors)
|
|
}
|
|
|
|
func main() {
|
|
go func() {
|
|
out := report{Images: map[string]string{}, PNG: map[string]string{}}
|
|
defer func() {
|
|
if p := recover(); p != nil {
|
|
out.Failures = append(out.Failures, fmt.Sprint(p))
|
|
}
|
|
b, _ := json.Marshal(out)
|
|
js.Global().Set("aegoMaterialResults", string(b))
|
|
}()
|
|
run(&out)
|
|
}()
|
|
select {}
|
|
}
|