Add a monitor renderer using TBOs (#443)

This uses the system described in #409, to render monitors in a more
efficient manner.

Each monitor is backed by a texture buffer object (TBO) which contains
a relatively compact encoding of the terminal state. This is then
rendered using a shader, which consumes the TBO and uses it to index
into main font texture.

As we're transmitting significantly less data to the GPU (only 3 bytes
per character), this effectively reduces any update lag to 0. FPS appears
to be up by a small fraction (10-15fps on my machine, to ~110), possibly
as we're now only drawing a single quad (though doing much more work in
the shader).

On my laptop, with its Intel integrated graphics card, I'm able to draw
120 full-sized monitors (with an effective resolution of 3972 x 2330) at
a consistent 60fps. Updates still cause a slight spike, but we always
remain above 30fps - a significant improvement over VBOs, where updates
would go off the chart.

Many thanks to @Lignum and @Lemmmy for devising this scheme, and helping
test and review it! ♥
This commit is contained in:
Jonathan Coates 2020-05-05 13:05:23 +01:00 committed by GitHub
parent 1547ecbeb3
commit 70b457ed18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 367 additions and 12 deletions

View File

@ -50,12 +50,12 @@ private FixedWidthFontRenderer()
{
}
private static float toGreyscale( double[] rgb )
public static float toGreyscale( double[] rgb )
{
return (float) ((rgb[0] + rgb[1] + rgb[2]) / 3);
}
private static int getColour( char c, Colour def )
public static int getColour( char c, Colour def )
{
return 15 - Terminal.getColour( c, def );
}

View File

@ -0,0 +1,176 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.client.render;
import com.google.common.base.Strings;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.client.gui.FixedWidthFontRenderer;
import dan200.computercraft.shared.util.Palette;
import net.minecraft.client.renderer.OpenGlHelper;
import org.apache.commons.io.IOUtils;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL20;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
class MonitorTextureBufferShader
{
static final int TEXTURE_INDEX = GL13.GL_TEXTURE3;
private static final FloatBuffer MATRIX_BUFFER = BufferUtils.createFloatBuffer( 16 );
private static final FloatBuffer PALETTE_BUFFER = BufferUtils.createFloatBuffer( 16 * 3 );
private static int uniformMv;
private static int uniformP;
private static int uniformFont;
private static int uniformWidth;
private static int uniformHeight;
private static int uniformTbo;
private static int uniformPalette;
private static boolean initialised;
private static boolean ok;
private static int program;
static void setupUniform( int width, int height, Palette palette, boolean greyscale )
{
MATRIX_BUFFER.rewind();
GL11.glGetFloat( GL11.GL_MODELVIEW_MATRIX, MATRIX_BUFFER );
MATRIX_BUFFER.rewind();
OpenGlHelper.glUniformMatrix4( uniformMv, false, MATRIX_BUFFER );
MATRIX_BUFFER.rewind();
GL11.glGetFloat( GL11.GL_PROJECTION_MATRIX, MATRIX_BUFFER );
MATRIX_BUFFER.rewind();
OpenGlHelper.glUniformMatrix4( uniformP, false, MATRIX_BUFFER );
OpenGlHelper.glUniform1i( uniformWidth, width );
OpenGlHelper.glUniform1i( uniformHeight, height );
PALETTE_BUFFER.rewind();
for( int i = 0; i < 16; i++ )
{
double[] colour = palette.getColour( i );
if( greyscale )
{
float f = FixedWidthFontRenderer.toGreyscale( colour );
PALETTE_BUFFER.put( f ).put( f ).put( f );
}
else
{
PALETTE_BUFFER.put( (float) colour[0] ).put( (float) colour[1] ).put( (float) colour[2] );
}
}
PALETTE_BUFFER.flip();
OpenGlHelper.glUniform3( uniformPalette, PALETTE_BUFFER );
}
static boolean use()
{
if( initialised )
{
if( ok ) OpenGlHelper.glUseProgram( program );
return ok;
}
if( ok = load() )
{
GL20.glUseProgram( program );
OpenGlHelper.glUniform1i( uniformFont, 0 );
OpenGlHelper.glUniform1i( uniformTbo, TEXTURE_INDEX - GL13.GL_TEXTURE0 );
}
return ok;
}
private static boolean load()
{
initialised = true;
try
{
int vertexShader = loadShader( GL20.GL_VERTEX_SHADER, "assets/computercraft/shaders/monitor.vert" );
int fragmentShader = loadShader( GL20.GL_FRAGMENT_SHADER, "assets/computercraft/shaders/monitor.frag" );
program = OpenGlHelper.glCreateProgram();
OpenGlHelper.glAttachShader( program, vertexShader );
OpenGlHelper.glAttachShader( program, fragmentShader );
GL20.glBindAttribLocation( program, 0, "v_pos" );
OpenGlHelper.glLinkProgram( program );
boolean ok = OpenGlHelper.glGetProgrami( program, GL20.GL_LINK_STATUS ) != 0;
String log = OpenGlHelper.glGetProgramInfoLog( program, Short.MAX_VALUE ).trim();
if( !Strings.isNullOrEmpty( log ) )
{
ComputerCraft.log.warn( "Problems when linking monitor shader: {}", log );
}
GL20.glDetachShader( program, vertexShader );
GL20.glDetachShader( program, fragmentShader );
OpenGlHelper.glDeleteShader( vertexShader );
OpenGlHelper.glDeleteShader( fragmentShader );
if( !ok ) return false;
uniformMv = getUniformLocation( program, "u_mv" );
uniformP = getUniformLocation( program, "u_p" );
uniformFont = getUniformLocation( program, "u_font" );
uniformWidth = getUniformLocation( program, "u_width" );
uniformHeight = getUniformLocation( program, "u_height" );
uniformTbo = getUniformLocation( program, "u_tbo" );
uniformPalette = getUniformLocation( program, "u_palette" );
ComputerCraft.log.info( "Loaded monitor shader." );
return true;
}
catch( Exception e )
{
ComputerCraft.log.error( "Cannot load monitor shaders", e );
return false;
}
}
private static int loadShader( int kind, String path ) throws IOException
{
InputStream stream = TileEntityMonitorRenderer.class.getClassLoader().getResourceAsStream( path );
if( stream == null ) throw new IllegalArgumentException( "Cannot find " + path );
byte[] contents = IOUtils.toByteArray( new BufferedInputStream( stream ) );
ByteBuffer buffer = BufferUtils.createByteBuffer( contents.length );
buffer.put( contents );
buffer.position( 0 );
int shader = OpenGlHelper.glCreateShader( kind );
OpenGlHelper.glShaderSource( shader, buffer );
OpenGlHelper.glCompileShader( shader );
boolean ok = OpenGlHelper.glGetShaderi( shader, GL20.GL_COMPILE_STATUS ) != 0;
String log = OpenGlHelper.glGetShaderInfoLog( shader, Short.MAX_VALUE ).trim();
if( !Strings.isNullOrEmpty( log ) )
{
ComputerCraft.log.warn( "Problems when loading monitor shader {}: {}", path, log );
}
if( !ok ) throw new IllegalStateException( "Cannot compile shader " + path );
return shader;
}
private static int getUniformLocation( int program, String name )
{
int uniform = OpenGlHelper.glGetUniformLocation( program, name );
if( uniform == -1 ) throw new IllegalStateException( "Cannot find uniform " + name );
return uniform;
}
}

View File

@ -8,23 +8,28 @@
import dan200.computercraft.client.FrameInfo;
import dan200.computercraft.client.gui.FixedWidthFontRenderer;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.terminal.TextBuffer;
import dan200.computercraft.shared.peripheral.monitor.ClientMonitor;
import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer;
import dan200.computercraft.shared.peripheral.monitor.TileMonitor;
import dan200.computercraft.shared.util.Colour;
import dan200.computercraft.shared.util.DirectionUtil;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.BufferBuilder;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.client.renderer.OpenGlHelper;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.client.renderer.*;
import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer;
import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
import net.minecraft.client.renderer.vertex.VertexBuffer;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.math.BlockPos;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL31;
import javax.annotation.Nonnull;
import java.nio.ByteBuffer;
import static dan200.computercraft.client.gui.FixedWidthFontRenderer.*;
import static dan200.computercraft.shared.peripheral.monitor.TileMonitor.RENDER_MARGIN;
public class TileEntityMonitorRenderer extends TileEntitySpecialRenderer<TileMonitor>
@ -97,8 +102,8 @@ private static void renderMonitorAt( TileMonitor monitor, double posX, double po
if( terminal != null )
{
// Draw a terminal
double xScale = xSize / (terminal.getWidth() * FixedWidthFontRenderer.FONT_WIDTH);
double yScale = ySize / (terminal.getHeight() * FixedWidthFontRenderer.FONT_HEIGHT);
double xScale = xSize / (terminal.getWidth() * FONT_WIDTH);
double yScale = ySize / (terminal.getHeight() * FONT_HEIGHT);
GlStateManager.pushMatrix();
GlStateManager.scale( (float) xScale, (float) -yScale, 1.0f );
@ -147,6 +152,52 @@ private static void renderTerminal( ClientMonitor monitor, float xMargin, float
switch( renderer )
{
case TBO:
{
if( !MonitorTextureBufferShader.use() ) return;
Terminal terminal = monitor.getTerminal();
int width = terminal.getWidth(), height = terminal.getHeight();
int pixelWidth = width * FONT_WIDTH, pixelHeight = height * FONT_HEIGHT;
if( redraw )
{
ByteBuffer monitorBuffer = GLAllocation.createDirectByteBuffer( width * height * 3 );
for( int y = 0; y < height; y++ )
{
TextBuffer text = terminal.getLine( y ), textColour = terminal.getTextColourLine( y ), background = terminal.getBackgroundColourLine( y );
for( int x = 0; x < width; x++ )
{
monitorBuffer.put( (byte) (text.charAt( x ) & 0xFF) );
monitorBuffer.put( (byte) getColour( textColour.charAt( x ), Colour.White ) );
monitorBuffer.put( (byte) getColour( background.charAt( x ), Colour.Black ) );
}
}
monitorBuffer.flip();
OpenGlHelper.glBindBuffer( GL31.GL_TEXTURE_BUFFER, monitor.tboBuffer );
OpenGlHelper.glBufferData( GL31.GL_TEXTURE_BUFFER, monitorBuffer, GL15.GL_STATIC_DRAW );
OpenGlHelper.glBindBuffer( GL31.GL_TEXTURE_BUFFER, 0 );
}
// Bind TBO texture and set up the uniforms. We've already set up the main font above.
GlStateManager.setActiveTexture( MonitorTextureBufferShader.TEXTURE_INDEX );
GL11.glBindTexture( GL31.GL_TEXTURE_BUFFER, monitor.tboTexture );
GlStateManager.setActiveTexture( GL13.GL_TEXTURE0 );
MonitorTextureBufferShader.setupUniform( width, height, terminal.getPalette(), !monitor.isColour() );
buffer.begin( GL11.GL_TRIANGLE_STRIP, DefaultVertexFormats.POSITION );
buffer.pos( -xMargin, -yMargin, 0 ).endVertex();
buffer.pos( -xMargin, pixelHeight + yMargin, 0 ).endVertex();
buffer.pos( pixelWidth + xMargin, -yMargin, 0 ).endVertex();
buffer.pos( pixelWidth + xMargin, pixelHeight + yMargin, 0 ).endVertex();
tessellator.draw();
OpenGlHelper.glUseProgram( 0 );
break;
}
case VBO:
{
VertexBuffer vbo = monitor.buffer;

View File

@ -8,10 +8,16 @@
import dan200.computercraft.client.gui.FixedWidthFontRenderer;
import dan200.computercraft.shared.common.ClientTerminal;
import net.minecraft.client.renderer.GLAllocation;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.client.renderer.OpenGlHelper;
import net.minecraft.client.renderer.vertex.VertexBuffer;
import net.minecraft.util.math.BlockPos;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL31;
import java.util.HashSet;
import java.util.Iterator;
@ -26,6 +32,8 @@ public final class ClientMonitor extends ClientTerminal
public long lastRenderFrame = -1;
public BlockPos lastRenderPos = null;
public int tboBuffer;
public int tboTexture;
public VertexBuffer buffer;
public int displayList = 0;
@ -43,7 +51,7 @@ public TileMonitor getOrigin()
/**
* Create the appropriate buffer if needed.
*
* @param renderer The renderer to use. This can be fetched from {@link #renderer()}.
* @param renderer The renderer to use. This can be fetched from {@link MonitorRenderer#current()}.
* @return If a buffer was created. This will return {@code false} if we already have an appropriate buffer,
* or this mode does not require one.
*/
@ -52,6 +60,26 @@ public boolean createBuffer( MonitorRenderer renderer )
{
switch( renderer )
{
case TBO:
{
if( tboBuffer != 0 ) return false;
deleteBuffers();
tboBuffer = OpenGlHelper.glGenBuffers();
OpenGlHelper.glBindBuffer( GL31.GL_TEXTURE_BUFFER, tboBuffer );
GL15.glBufferData( GL31.GL_TEXTURE_BUFFER, 0, GL15.GL_STATIC_DRAW );
tboTexture = GlStateManager.generateTexture();
GL11.glBindTexture( GL31.GL_TEXTURE_BUFFER, tboTexture );
GL31.glTexBuffer( GL31.GL_TEXTURE_BUFFER, GL30.GL_R8, tboBuffer );
GL11.glBindTexture( GL31.GL_TEXTURE_BUFFER, 0 );
OpenGlHelper.glBindBuffer( GL31.GL_TEXTURE_BUFFER, 0 );
addMonitor();
return true;
}
case VBO:
if( buffer != null ) return false;
@ -59,6 +87,7 @@ public boolean createBuffer( MonitorRenderer renderer )
buffer = new VertexBuffer( FixedWidthFontRenderer.POSITION_COLOR_TEX );
addMonitor();
return true;
case DISPLAY_LIST:
if( displayList != 0 ) return false;
@ -82,6 +111,19 @@ private void addMonitor()
private void deleteBuffers()
{
if( tboBuffer != 0 )
{
OpenGlHelper.glDeleteBuffers( tboBuffer );
tboBuffer = 0;
}
if( tboTexture != 0 )
{
GlStateManager.deleteTexture( tboTexture );
tboTexture = 0;
}
if( buffer != null )
{
buffer.deleteGlBuffers();
@ -98,7 +140,7 @@ private void deleteBuffers()
@SideOnly( Side.CLIENT )
public void destroy()
{
if( buffer != null || displayList != 0 )
if( tboBuffer != 0 || buffer != null || displayList != 0 )
{
synchronized( allMonitors )
{

View File

@ -9,6 +9,7 @@
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.client.render.TileEntityMonitorRenderer;
import net.minecraft.client.renderer.OpenGlHelper;
import org.lwjgl.opengl.GLContext;
import javax.annotation.Nonnull;
import java.util.Locale;
@ -27,7 +28,14 @@ public enum MonitorRenderer
BEST,
/**
* Render using VBOs. This is the default when supported.
* Render using texture buffer objects.
*
* @see org.lwjgl.opengl.GL31#glTexBuffer(int, int, int)
*/
TBO,
/**
* Render using VBOs.
*
* @see net.minecraft.client.renderer.vertex.VertexBuffer
*/
@ -84,6 +92,16 @@ public static MonitorRenderer current()
{
case BEST:
return best();
case TBO:
checkCapabilities();
if( !textureBuffer )
{
ComputerCraft.log.warn( "Texture buffers are not supported on your graphics card. Falling back to default." );
ComputerCraft.monitorRenderer = BEST;
return best();
}
return TBO;
case VBO:
if( !OpenGlHelper.vboSupported )
{
@ -100,6 +118,20 @@ public static MonitorRenderer current()
private static MonitorRenderer best()
{
return OpenGlHelper.vboSupported ? VBO : DISPLAY_LIST;
checkCapabilities();
if( textureBuffer ) return TBO;
if( OpenGlHelper.vboSupported ) return VBO;
return DISPLAY_LIST;
}
private static boolean initialised = false;
private static boolean textureBuffer = false;
private static void checkCapabilities()
{
if( initialised ) return;
textureBuffer = GLContext.getCapabilities().OpenGL31;
initialised = true;
}
}

View File

@ -187,6 +187,7 @@ gui.computercraft:config.peripheral.modem_high_altitude_range_during_storm=Modem
gui.computercraft:config.peripheral.max_notes_per_tick=Maximum notes that a computer can play at once
gui.computercraft:config.peripheral.monitor_renderer=Monitor renderer
gui.computercraft:config.peripheral.monitor_renderer.best=Best
gui.computercraft:config.peripheral.monitor_renderer.tbo=Texture Buffers
gui.computercraft:config.peripheral.monitor_renderer.vbo=Vertex Buffers
gui.computercraft:config.peripheral.monitor_renderer.display_list=Display Lists

View File

@ -0,0 +1,40 @@
#version 140
#define FONT_WIDTH 6.0
#define FONT_HEIGHT 9.0
uniform sampler2D u_font;
uniform int u_width;
uniform int u_height;
uniform samplerBuffer u_tbo;
uniform vec3 u_palette[16];
in vec2 f_pos;
out vec4 colour;
vec2 texture_corner(int index) {
float x = 1.0 + float(index % 16) * (FONT_WIDTH + 2.0);
float y = 1.0 + float(index / 16) * (FONT_HEIGHT + 2.0);
return vec2(x, y);
}
void main() {
vec2 term_pos = vec2(f_pos.x / FONT_WIDTH, f_pos.y / FONT_HEIGHT);
vec2 corner = floor(term_pos);
ivec2 cell = ivec2(corner);
int index = 3 * (clamp(cell.x, 0, u_width - 1) + clamp(cell.y, 0, u_height - 1) * u_width);
// 1 if 0 <= x, y < width, height, 0 otherwise
vec2 outside = step(vec2(0.0, 0.0), vec2(cell)) * step(vec2(cell), vec2(float(u_width) - 1.0, float(u_height) - 1.0));
float mult = outside.x * outside.y;
int character = int(texelFetch(u_tbo, index).r * 255.0);
int fg = int(texelFetch(u_tbo, index + 1).r * 255.0);
int bg = int(texelFetch(u_tbo, index + 2).r * 255.0);
vec2 pos = (term_pos - corner) * vec2(FONT_WIDTH, FONT_HEIGHT);
vec4 img = texture2D(u_font, (texture_corner(character) + pos) / 256.0);
colour = vec4(mix(u_palette[bg], img.rgb * u_palette[fg], img.a * mult), 1.0);
}

View File

@ -0,0 +1,13 @@
#version 140
uniform mat4 u_mv;
uniform mat4 u_p;
in vec3 v_pos;
out vec2 f_pos;
void main() {
gl_Position = u_p * u_mv * vec4(v_pos.x, v_pos.y, 0, 1);
f_pos = v_pos.xy;
}