/**
* Copyright 2010 The ForPlay Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package forplay.html;
import static com.google.gwt.webgl.client.WebGLRenderingContext.*;
import com.google.gwt.dom.client.CanvasElement;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.ImageElement;
import com.google.gwt.typedarrays.client.Float32Array;
import com.google.gwt.typedarrays.client.Int32Array;
import com.google.gwt.typedarrays.client.Uint16Array;
import com.google.gwt.typedarrays.client.Uint8Array;
import com.google.gwt.webgl.client.WebGLBuffer;
import com.google.gwt.webgl.client.WebGLContextAttributes;
import com.google.gwt.webgl.client.WebGLFramebuffer;
import com.google.gwt.webgl.client.WebGLProgram;
import com.google.gwt.webgl.client.WebGLRenderingContext;
import com.google.gwt.webgl.client.WebGLTexture;
import com.google.gwt.webgl.client.WebGLUniformLocation;
import com.google.gwt.webgl.client.WebGLUtil;
import forplay.core.CanvasLayer;
import forplay.core.ForPlay;
import forplay.core.GroupLayer;
import forplay.core.Image;
import forplay.core.ImageLayer;
import forplay.core.SurfaceLayer;
import forplay.core.Transform;
class HtmlGraphicsGL extends HtmlGraphics {
private static final int VERTEX_SIZE = 10; // 10 floats per vertex
// TODO(jgw): Re-enable longer element buffers once we figure out why they're causing weird
// performance degradation.
// private static final int MAX_VERTS = 400; // 100 quads
// private static final int MAX_ELEMS = MAX_VERTS * 6 / 4; // At most 6 verts per quad
// These values allow only one quad at a time (there's no generalized polygon rendering available
// in Surface yet that would use more than 4 points / 2 triangles).
private static final int MAX_VERTS = 4;
private static final int MAX_ELEMS = 6;
private class Shader {
WebGLProgram program;
WebGLUniformLocation uScreenSizeLoc;
int aMatrix, aTranslation, aPosition, aTexture;
WebGLBuffer buffer, indexBuffer;
Float32Array vertexData = Float32Array.create(VERTEX_SIZE * MAX_VERTS);
Uint16Array elementData = Uint16Array.create(MAX_ELEMS);
int vertexOffset, elementOffset;
Shader(String fragmentShader) {
// Compile the shader.
String vertexShader = Shaders.INSTANCE.vertexShader().getText();
program = WebGLUtil.createShaderProgram(gl, vertexShader, fragmentShader);
// glGet*() calls are slow; determine locations once.
uScreenSizeLoc = gl.getUniformLocation(program, "u_ScreenSize");
aMatrix = gl.getAttribLocation(program, "a_Matrix");
aTranslation = gl.getAttribLocation(program, "a_Translation");
aPosition = gl.getAttribLocation(program, "a_Position");
aTexture = gl.getAttribLocation(program, "a_Texture");
// Create the vertex and index buffers.
buffer = gl.createBuffer();
indexBuffer = gl.createBuffer();
}
boolean prepare() {
if (useShader(this)) {
gl.useProgram(program);
gl.uniform2fv(uScreenSizeLoc, new float[] { screenWidth, screenHeight });
gl.bindBuffer(ARRAY_BUFFER, buffer);
gl.bindBuffer(ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.enableVertexAttribArray(aMatrix);
gl.enableVertexAttribArray(aTranslation);
gl.enableVertexAttribArray(aPosition);
if (aTexture != -1) {
gl.enableVertexAttribArray(aTexture);
}
gl.vertexAttribPointer(aMatrix, 4, FLOAT, false, 40, 0);
gl.vertexAttribPointer(aTranslation, 2, FLOAT, false, 40, 16);
gl.vertexAttribPointer(aPosition, 2, FLOAT, false, 40, 24);
if (aTexture != -1) {
gl.vertexAttribPointer(aTexture, 2, FLOAT, false, 40, 32);
}
return true;
}
return false;
}
void flush() {
if (vertexOffset == 0) {
return;
}
// TODO(jgw): Change this back. It only works because we've limited MAX_VERTS, which only
// works because there are no >4 vertex draws happening.
// gl.bufferData(ARRAY_BUFFER, vertexData.subarray(0, vertexOffset), STREAM_DRAW);
// gl.bufferData(ELEMENT_ARRAY_BUFFER, elementData.subarray(0, elementOffset), STREAM_DRAW);
gl.bufferData(ARRAY_BUFFER, vertexData, STREAM_DRAW);
gl.bufferData(ELEMENT_ARRAY_BUFFER, elementData, STREAM_DRAW);
gl.drawElements(TRIANGLES, elementOffset, UNSIGNED_SHORT, 0);
vertexOffset = elementOffset = 0;
}
int beginPrimitive(int vertexCount, int elemCount) {
int vertIdx = vertexOffset / VERTEX_SIZE;
if ((vertIdx + vertexCount > MAX_VERTS) || (elementOffset + elemCount > MAX_ELEMS)) {
flush();
return 0;
}
return vertIdx;
}
void buildVertex(Transform local, float dx, float dy) {
buildVertex(local, dx, dy, 0, 0);
}
void buildVertex(Transform local, float dx, float dy, float sx, float sy) {
vertexData.set(vertexOffset + 0, local.m00());
vertexData.set(vertexOffset + 1, local.m01());
vertexData.set(vertexOffset + 2, local.m10());
vertexData.set(vertexOffset + 3, local.m11());
vertexData.set(vertexOffset + 4, local.tx());
vertexData.set(vertexOffset + 5, local.ty());
vertexData.set(vertexOffset + 6, dx);
vertexData.set(vertexOffset + 7, dy);
vertexData.set(vertexOffset + 8, sx);
vertexData.set(vertexOffset + 9, sy);
vertexOffset += VERTEX_SIZE;
}
void addElement(int index) {
elementData.set(elementOffset++, index);
}
}
private class TextureShader extends Shader {
WebGLUniformLocation uTexture;
WebGLUniformLocation uAlpha;
WebGLTexture lastTex;
float lastAlpha;
TextureShader() {
super(Shaders.INSTANCE.texFragmentShader().getText());
uTexture = gl.getUniformLocation(program, "u_Texture");
uAlpha = gl.getUniformLocation(program, "u_Alpha");
}
@Override
void flush() {
gl.bindTexture(TEXTURE_2D, lastTex);
super.flush();
}
void prepare(WebGLTexture tex, float alpha) {
if (super.prepare()) {
gl.activeTexture(TEXTURE0);
gl.uniform1i(uTexture, 0);
}
if (tex == lastTex && alpha == lastAlpha) {
return;
}
flush();
gl.uniform1f(uAlpha, alpha);
lastAlpha = alpha;
lastTex = tex;
}
}
private class ColorShader extends Shader {
WebGLUniformLocation uColor;
WebGLUniformLocation uAlpha;
Float32Array colors = Float32Array.create(4);
int lastColor;
float lastAlpha;
ColorShader() {
super(Shaders.INSTANCE.colorFragmentShader().getText());
uColor = gl.getUniformLocation(program, "u_Color");
uAlpha = gl.getUniformLocation(program, "u_Alpha");
}
void prepare(int color, float alpha) {
super.prepare();
if (color == lastColor && alpha == lastAlpha) {
return;
}
flush();
gl.uniform1f(uAlpha, alpha);
lastAlpha = alpha;
setColor(color);
}
private void setColor(int color) {
// ABGR.
colors.set(3, (float)((color >> 24) & 0xff) / 255);
colors.set(0, (float)((color >> 16) & 0xff) / 255);
colors.set(1, (float)((color >> 8) & 0xff) / 255);
colors.set(2, (float)((color >> 0) & 0xff) / 255);
gl.uniform4fv(uColor, colors);
lastColor = color;
}
}
WebGLRenderingContext gl;
private WebGLFramebuffer lastFBuf;
private int screenWidth, screenHeight;
private HtmlGroupLayerGL rootLayer;
private CanvasElement canvas;
// Shaders & Meshes.
private Shader curShader;
private TextureShader texShader;
private ColorShader colorShader;
// Debug counters.
private int texCount;
HtmlGraphicsGL() {
rootLayer = new HtmlGroupLayerGL(this);
createCanvas();
initGL();
texShader = new TextureShader();
colorShader = new ColorShader();
setSize(HtmlPlatform.DEFAULT_WIDTH, HtmlPlatform.DEFAULT_HEIGHT);
}
@Override
public CanvasLayer createCanvasLayer(int width, int height) {
return new HtmlCanvasLayerGL(this, width, height);
}
@Override
public GroupLayer createGroupLayer() {
return new HtmlGroupLayerGL(this);
}
@Override
public ImageLayer createImageLayer() {
return new HtmlImageLayerGL(this);
}
@Override
public ImageLayer createImageLayer(Image img) {
return new HtmlImageLayerGL(this, img);
}
@Override
public SurfaceLayer createSurfaceLayer(int width, int height) {
return new HtmlSurfaceLayerGL(this, width, height);
}
@Override
public int height() {
return canvas.getOffsetHeight();
}
@Override
public HtmlGroupLayerGL rootLayer() {
return rootLayer;
}
@Override
public void setSize(int width, int height) {
super.setSize(width, height);
canvas.setWidth(width);
canvas.setHeight(height);
bindFramebuffer(null, width, height, true);
}
@Override
public int width() {
return canvas.getOffsetWidth();
}
void bindFramebuffer() {
bindFramebuffer(null, canvas.getWidth(), canvas.getHeight());
}
void bindFramebuffer(WebGLFramebuffer fbuf, int width, int height) {
bindFramebuffer(fbuf, width, height, false);
}
void bindFramebuffer(WebGLFramebuffer fbuf, int width, int height, boolean force) {
if (force || lastFBuf != fbuf) {
flush();
lastFBuf = fbuf;
gl.bindFramebuffer(FRAMEBUFFER, fbuf);
gl.viewport(0, 0, width, height);
screenWidth = width;
screenHeight = height;
}
}
WebGLTexture createTexture(boolean repeatX, boolean repeatY) {
WebGLTexture tex = gl.createTexture();
gl.bindTexture(TEXTURE_2D, tex);
gl.texParameteri(TEXTURE_2D, TEXTURE_MAG_FILTER, LINEAR);
gl.texParameteri(TEXTURE_2D, TEXTURE_MIN_FILTER, LINEAR);
gl.texParameteri(TEXTURE_2D, TEXTURE_WRAP_S, repeatX ? REPEAT : CLAMP_TO_EDGE);
gl.texParameteri(TEXTURE_2D, TEXTURE_WRAP_T, repeatY ? REPEAT : CLAMP_TO_EDGE);
++texCount;
return tex;
}
void destroyTexture(WebGLTexture tex) {
gl.deleteTexture(tex);
--texCount;
}
void updateLayers() {
bindFramebuffer(null, canvas.getWidth(), canvas.getHeight());
// Clear to transparent.
gl.clear(COLOR_BUFFER_BIT);
// Paint all the layers.
rootLayer.paint(gl, Transform.IDENTITY, 1);
// Guarantee a flush.
useShader(null);
}
public void updateTexture(WebGLTexture tex, Element img) {
gl.bindTexture(TEXTURE_2D, tex);
gl.texImage2D(TEXTURE_2D, 0, RGBA, RGBA, UNSIGNED_BYTE, img.<ImageElement>cast());
}
void drawTexture(WebGLTexture tex, float texWidth, float texHeight, Transform local, float dw,
float dh, boolean repeatX, boolean repeatY, float alpha) {
drawTexture(tex, texWidth, texHeight, local, 0, 0, dw, dh, repeatX, repeatY, alpha);
}
void drawTexture(WebGLTexture tex, float texWidth, float texHeight, Transform local, float dx,
float dy, float dw, float dh, boolean repeatX, boolean repeatY, float alpha) {
float sw = repeatX ? dw : texWidth, sh = repeatY ? dh : texHeight;
drawTexture(tex, texWidth, texHeight, local, dx, dy, dw, dh, 0, 0, sw, sh, alpha);
}
void drawTexture(WebGLTexture tex, float texWidth, float texHeight, Transform local, float dx,
float dy, float dw, float dh, float sx, float sy, float sw, float sh, float alpha) {
texShader.prepare(tex, alpha);
sx /= texWidth; sw /= texWidth;
sy /= texHeight; sh /= texHeight;
int idx = texShader.beginPrimitive(4, 6);
texShader.buildVertex(local, dx, dy, sx, sy);
texShader.buildVertex(local, dx + dw, dy, sx + sw, sy);
texShader.buildVertex(local, dx, dy + dh, sx, sy + sh);
texShader.buildVertex(local, dx + dw, dy + dh, sx + sw, sy + sh);
texShader.addElement(idx + 0); texShader.addElement(idx + 1); texShader.addElement(idx + 2);
texShader.addElement(idx + 1); texShader.addElement(idx + 3); texShader.addElement(idx + 2);
}
void fillRect(Transform local, float dx, float dy, float dw, float dh, float texWidth,
float texHeight, WebGLTexture tex, float alpha) {
texShader.prepare(tex, alpha);
float sx = dx / texWidth, sy = dy / texHeight;
float sw = dw / texWidth, sh = dh / texHeight;
int idx = texShader.beginPrimitive(4, 6);
texShader.buildVertex(local, dx, dy, sx, sy);
texShader.buildVertex(local, dx + dw, dy, sx + sw, sy);
texShader.buildVertex(local, dx, dy + dh, sx, sy + sh);
texShader.buildVertex(local, dx + dw, dy + dh, sx + sw, sy + sh);
texShader.addElement(idx + 0); texShader.addElement(idx + 1); texShader.addElement(idx + 2);
texShader.addElement(idx + 1); texShader.addElement(idx + 3); texShader.addElement(idx + 2);
}
void fillRect(Transform local, float dx, float dy, float dw, float dh, int color, float alpha) {
colorShader.prepare(color, alpha);
int idx = colorShader.beginPrimitive(4, 6);
colorShader.buildVertex(local, dx, dy);
colorShader.buildVertex(local, dx + dw, dy);
colorShader.buildVertex(local, dx, dy + dh);
colorShader.buildVertex(local, dx + dw, dy + dh);
colorShader.addElement(idx + 0);
colorShader.addElement(idx + 1);
colorShader.addElement(idx + 2);
colorShader.addElement(idx + 1);
colorShader.addElement(idx + 3);
colorShader.addElement(idx + 2);
}
void fillPoly(Transform local, float[] positions, int color, float alpha) {
colorShader.prepare(color, alpha);
int idx = colorShader.beginPrimitive(4, 6);
int points = positions.length / 2;
for (int i = 0; i < points; ++i) {
float dx = positions[i * 2];
float dy = positions[i * 2 + 1];
colorShader.buildVertex(local, dx, dy);
}
int a = idx + 0, b = idx + 1, c = idx + 2;
int tris = points - 2;
for (int i = 0; i < tris; ++i) {
colorShader.addElement(a); colorShader.addElement(b); colorShader.addElement(c);
a = c;
b = a + 1;
c = (i == tris - 2) ? idx : b + 1;
}
}
@Override
Element getRootElement() {
return canvas;
}
void flush() {
if (curShader != null) {
curShader.flush();
curShader = null;
}
}
private void createCanvas() {
canvas = Document.get().createCanvasElement();
rootElement.appendChild(canvas);
}
private void initGL() {
if (!tryCreateContext(null)) {
giveUp();
}
gl.disable(CULL_FACE);
gl.enable(BLEND);
gl.blendEquation(FUNC_ADD);
gl.blendFuncSeparate(SRC_ALPHA, ONE_MINUS_SRC_ALPHA, SRC_ALPHA, DST_ALPHA);
if (!tryBasicGlCalls()) {
giveUp();
}
}
private boolean tryCreateContext(WebGLContextAttributes attrs) {
// Try to create a context. If this returns null, then the browser doesn't support WebGL
// on this machine.
gl = WebGLRenderingContext.getContext(canvas, attrs);
if (gl == null) {
return false;
}
// Some systems seem to have a problem where they return a valid context, but it's in an error
// static initially. We give up and fall back to dom/canvas in this case, because nothing seems
// to work properly.
return (gl.getError() == NO_ERROR);
}
/**
* Try basic GL operations to detect failure cases early.
*
* @return true if calls succeed, false otherwise.
*/
private boolean tryBasicGlCalls() {
int err;
try {
// test that our Float32 arrays work (a technique found in other WebGL checks)
Float32Array testFloat32Array = Float32Array.create(new float[]{0.0f, 1.0f, 2.0f});
if (testFloat32Array.get(0) != 0.0f || testFloat32Array.get(1) != 1.0f
|| testFloat32Array.get(2) != 2.0f) {
throw new RuntimeException("Typed Float32Array check failed");
}
// test that our Int32 arrays work
Int32Array testInt32Array = Int32Array.create(new int[]{0, 1, 2});
if (testInt32Array.get(0) != 0 || testInt32Array.get(1) != 1 || testInt32Array.get(2) != 2) {
throw new RuntimeException("Typed Int32Array check failed");
}
// test that our Uint16 arrays work
Uint16Array testUint16Array = Uint16Array.create(new int[]{0, 1, 2});
if (testUint16Array.get(0) != 0 || testUint16Array.get(1) != 1 ||
testUint16Array.get(2) != 2) {
throw new RuntimeException("Typed Uint16Array check failed");
}
// test that our Uint8 arrays work
Uint8Array testUint8Array = Uint8Array.create(new int[]{0, 1, 2});
if (testUint8Array.get(0) != 0 || testUint8Array.get(1) != 1 || testUint8Array.get(2) != 2) {
throw new RuntimeException("Typed Uint8Array check failed");
}
// Perform GL read back test where we paint rgba(1, 1, 1, 1) and then read back that data.
// (should be 100% opaque white).
bindFramebuffer();
gl.clearColor(1, 1, 1, 1);
err = gl.getError();
if (err != NO_ERROR) {
throw new RuntimeException("Read back GL test failed to clear color (error " + err + ")");
}
updateLayers();
Uint8Array pixelData = Uint8Array.create(4);
gl.readPixels(0, 0, 1, 1, RGBA, UNSIGNED_BYTE, pixelData);
if (pixelData.get(0) != 255 || pixelData.get(1) != 255 || pixelData.get(2) != 255) {
throw new RuntimeException("Read back GL test failed to read back correct color");
}
} catch (RuntimeException e) {
ForPlay.log().info("Basic GL check failed: " + e.getMessage());
return false;
} catch (Throwable t) {
ForPlay.log().info("Basic GL check failed with an unknown error: " + t.getMessage());
return false;
}
return true;
}
private void giveUp() {
// Give up. HtmlPlatform will catch the exception and fall back to dom/canvas.
rootElement.removeChild(canvas);
throw new RuntimeException();
}
private boolean useShader(Shader shader) {
if (curShader != shader) {
flush();
curShader = shader;
return true;
}
return false;
}
@SuppressWarnings("unused")
private void printArray(String prefix, Float32Array arr, int length) {
StringBuffer buf = new StringBuffer();
buf.append(prefix + ": [");
for (int i = 0; i < length; ++i) {
buf.append(arr.get(i) + " ");
}
buf.append("]");
System.out.println(buf);
}
@SuppressWarnings("unused")
private void printArray(String prefix, Uint16Array arr, int length) {
StringBuffer buf = new StringBuffer();
buf.append(prefix + ": [");
for (int i = 0; i < length; ++i) {
buf.append(arr.get(i) + " ");
}
buf.append("]");
System.out.println(buf);
}
}