Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,35 @@ Pixel is in development and still missing few critical features. Here're the mos
- Antialiasing (filtering is supported, though)
- ~~Advanced window manipulation (cursor hiding, window icon, ...)~~
- Better support for Hi-DPI displays
- Mobile (and perhaps HTML5?) backend
- Mobile backend (an HTML5/WebGL2 backend ships under `GOOS=js GOARCH=wasm`)
- ~~More advanced graphical effects (e.g. blur)~~ (solved with the addition of GLSL effects)
- Tests and benchmarks
- Vulkan support

**Implementing these features will get us to the 1.0 release.** Contribute, so that it's as soon as
possible!

## WebAssembly (browser)

Pixel ships a WebGL2 backend under the `js && wasm` build tag, so a game
written against the standard `opengl` backend can also be compiled for the
browser:

```sh
GOOS=js GOARCH=wasm go build -o game.wasm ./cmd/game
```

Load `game.wasm` from an HTML page that also serves Go's `wasm_exec.js` and
provides a canvas element. The backend attaches to `<canvas id="game">` by
default; override by setting `opengl.CanvasElementID` before calling
`opengl.NewWindow`.

Some desktop-oriented features are stubbed in the browser: multi-monitor
queries, joysticks/gamepads, and custom cursor images are no-ops, and window
positioning is driven by CSS rather than by `SetPos`. Keyboard, mouse (buttons,
movement, scroll), text input, and resizing (including fullscreen) are wired
up through DOM events.

## Requirements

If you're using Windows and having trouble building Pixel, please check [this guide](./docs/Compilation/Building-Pixel-on-Windows.md) on the [wiki](./docs/README.md).
Expand Down
27 changes: 25 additions & 2 deletions backends/opengl/canvas.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,11 @@ func setBlendFunc(cmp pixel.ComposeMethod) {
glhf.BlendFunc(glhf.One, glhf.One)
case pixel.ComposeCopy:
glhf.BlendFunc(glhf.One, glhf.Zero)
case pixel.ComposeMultiply:
glhf.BlendFunc(glhf.DstColor, glhf.Zero)
case pixel.ComposeScreen:
glhf.BlendEquation(glhf.FuncAdd)
glhf.BlendFuncSeparate(glhf.One, glhf.OneMinusSrcColor, glhf.Zero, glhf.One)
default:
panic(errors.New("Canvas: invalid compose method"))
}
Expand All @@ -203,7 +208,11 @@ func (c *Canvas) Clear(color color.Color) {
A: float64(c.col[3]),
})

mainthread.CallNonBlock(func() {
// Use Call (blocking) not CallNonBlock: on WASM, CallNonBlock spawns a
// goroutine, and every WebGL call is a goroutine scheduling point — the
// deferred clear would race with in-flight draw calls and wipe content
// mid-render.
mainthread.Call(func() {
c.setGlhfBounds()
c.gf.Frame().Begin()
glhf.Clear(
Expand Down Expand Up @@ -288,7 +297,11 @@ func (ct *canvasTriangles) draw(tex *glhf.Texture, bounds pixel.Rect) {
mat := ct.dst.mat
col := ct.dst.col

mainthread.CallNonBlock(func() {
// Use Call (blocking) not CallNonBlock: same race as Canvas.Clear — on
// WASM every WebGL call is a goroutine scheduling point, so a non-blocking
// draw can be preempted by window.Update (SwapBuffers) before it executes,
// producing a black frame.
mainthread.Call(func() {
ct.dst.setGlhfBounds()
setBlendFunc(cmp)

Expand Down Expand Up @@ -317,8 +330,18 @@ func (ct *canvasTriangles) draw(tex *glhf.Texture, bounds pixel.Rect) {
}

for loc, u := range ct.shader.uniforms {
<<<<<<< HEAD
ct.shader.s.SetUniformAttr(loc, u.Value())
}
=======
if u.isSampler && u.tex != nil {
glhf.ActiveTexture(u.unit)
u.tex.Begin()
}
ct.shader.s.SetUniformAttr(loc, u.Value())
}
glhf.ActiveTexture(0)
>>>>>>> 6987ad7 (Fix Canvas.Clear race on WASM: use blocking Call not CallNonBlock)

if tex == nil {
ct.vs.Begin()
Expand Down
2 changes: 2 additions & 0 deletions backends/opengl/cursor.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !js

package opengl

import (
Expand Down
31 changes: 31 additions & 0 deletions backends/opengl/cursor_wasm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//go:build js && wasm

package opengl

import (
"image"

"github.com/gopxl/pixel/v2"
)

// StandardCursor matches the desktop-side typed constant. Under WASM we have
// no real cursor API wired up, so the constants exist only so game code can
// reference them unchanged.
type StandardCursor int

const (
ArrowCursor StandardCursor = iota
IBeamCursor
CrosshairCursor
HandCursor
HResizeCursor
VResizeCursor
)

// Cursor is an opaque handle. We keep the struct empty; the browser manages
// the mouse cursor itself.
type Cursor struct{}

func CreateStandardCursor(StandardCursor) *Cursor { return &Cursor{} }
func CreateCursorImage(image.Image, pixel.Vec) *Cursor { return &Cursor{} }
func (w *Window) SetCursor(*Cursor) {}
15 changes: 15 additions & 0 deletions backends/opengl/glpicture.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package opengl

import (
"math"
"sync"

"github.com/gopxl/glhf/v2"
"github.com/gopxl/mainthread/v2"
Expand All @@ -18,9 +19,20 @@ type GLPicture interface {
Texture() *glhf.Texture
}

// pictureDataGLCache memoizes GLPicture uploads keyed by source *pixel.PictureData.
// Without this, every (sprite, canvas) pair re-uploads and retains a full-size pixel
// copy; on 32-bit WASM (~4GB heap) a 4096x4096 atlas blows the heap after a handful
// of draw targets.
var pictureDataGLCache sync.Map // *pixel.PictureData -> GLPicture

// NewGLPicture creates a new GLPicture with it's own static OpenGL texture. This function always
// allocates a new texture that cannot (shouldn't) be further modified.
func NewGLPicture(p pixel.Picture) GLPicture {
if pd, ok := p.(*pixel.PictureData); ok {
if cached, found := pictureDataGLCache.Load(pd); found {
return cached.(GLPicture)
}
}
bounds := p.Bounds()
bx, by, bw, bh := intBounds(bounds)

Expand Down Expand Up @@ -65,6 +77,9 @@ func NewGLPicture(p pixel.Picture) GLPicture {
tex: tex,
pixels: pixels,
}
if pd, ok := p.(*pixel.PictureData); ok {
pictureDataGLCache.Store(pd, gp)
}
return gp
}

Expand Down
10 changes: 5 additions & 5 deletions backends/opengl/glshader.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ uniform vec4 uBounds;

void main() {
vec2 transPos = (uTransform * vec3(aPosition, 1.0)).xy;
vec2 normPos = (transPos - uBounds.xy) / uBounds.zw * 2 - vec2(1, 1);
vec2 normPos = (transPos - uBounds.xy) / uBounds.zw * 2.0 - vec2(1.0, 1.0);
gl_Position = vec4(normPos, 0.0, 1.0);

vColor = aColor;
Expand All @@ -282,14 +282,14 @@ uniform vec4 uTexBounds;
uniform sampler2D uTexture;

void main() {
if ((vClipRect != vec4(0,0,0,0)) && (gl_FragCoord.x < vClipRect.x || gl_FragCoord.y < vClipRect.y || gl_FragCoord.x > vClipRect.z || gl_FragCoord.y > vClipRect.w))
if ((vClipRect != vec4(0.0, 0.0, 0.0, 0.0)) && (gl_FragCoord.x < vClipRect.x || gl_FragCoord.y < vClipRect.y || gl_FragCoord.x > vClipRect.z || gl_FragCoord.y > vClipRect.w))
discard;

if (vIntensity == 0) {
if (vIntensity == 0.0) {
fragColor = uColorMask * vColor;
} else {
fragColor = vec4(0, 0, 0, 0);
fragColor += (1 - vIntensity) * vColor;
fragColor = vec4(0.0, 0.0, 0.0, 0.0);
fragColor += (1.0 - vIntensity) * vColor;
vec2 t = (vTexCoords - uTexBounds.xy) / uTexBounds.zw;
fragColor += vIntensity * vColor * texture(uTexture, t);
fragColor *= uColorMask;
Expand Down
24 changes: 8 additions & 16 deletions backends/opengl/gltriangles.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,22 +201,14 @@ func (gt *GLTriangles) Update(t pixel.Triangles) {

// CopyVertices copies the GLTriangle data down to the vertex data.
func (gt *GLTriangles) CopyVertices() {
// this code is supposed to copy the vertex data and CallNonBlock the update if
// the data is small enough, otherwise it'll block and not copy the data
if len(gt.data) < 256 { // arbitrary heurestic constant
data := append([]float32{}, gt.data...)
mainthread.CallNonBlock(func() {
gt.vs.Begin()
gt.vs.SetVertexData(data)
gt.vs.End()
})
} else {
mainthread.Call(func() {
gt.vs.Begin()
gt.vs.SetVertexData(gt.data)
gt.vs.End()
})
}
// Always use blocking Call. On WASM, CallNonBlock spawns a goroutine that
// can be preempted by a subsequent draw or SwapBuffers before the vertex
// upload completes, causing draws with stale or empty buffers.
mainthread.Call(func() {
gt.vs.Begin()
gt.vs.SetVertexData(gt.data)
gt.vs.End()
})
}

// Copy returns an independent copy of this GLTriangles.
Expand Down
2 changes: 2 additions & 0 deletions backends/opengl/input.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !js

package opengl

import (
Expand Down
Loading