feat(3d): gl1 shim M3+M5 — display-list recorder, client arrays, VBO routing; 44/47 under parity floor

- display lists: literal command replay through the shim state machine
  (state leaks + between-list material changes get GL semantics for free);
  recorded glDrawArrays EAGERLY snapshots enabled client-memory arrays at
  compile time (the renderer frees them right after glEndList) and records
  buffer+offset for VBO-backed pointers
- client-array draws: shared AttribSource path packs client-memory arrays
  into one scratch-VBO upload and binds user VBOs with original offsets
  (GL_BYTE normals / GL_UNSIGNED_BYTE colors normalized per GL1)
- glDrawElements over user VBO/IBO passes indices through untouched
  (ELEMENT_ARRAY_BUFFER never touched by the shim)
- state mutators (enable/bindTexture/blendFunc/lineWidth/alphaFunc) shared
  between the __wrap interceptors and list replay

Parity: 44/47 under the 0.02 floor (was 20). Verified visually: mini-board
Tier-3 composite (mask translucency + stencil-punched TH holes), stencil
hole subtraction, alpha-tested seg-end discs, COMBINE const-alpha model
transparency, and post-machining reproducing the upstream countersink bug
(walls absent, matching the native golden). Remaining 3: camera-* trio
needing the M4 GLU quadrics (+5 small-geometry false-greens they share).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Istvan Matejcsok 2026-07-03 11:48:22 +02:00
commit 3fe4f6fb00
6 changed files with 565 additions and 87 deletions

View file

@ -209,6 +209,35 @@ bool* ffpCapSlot( GLenum cap );
// Marks the state blocks a cap flip invalidates.
void onCapChanged( GLenum cap );
// Immediate-execution state mutators shared by the __wrap_* interceptors and
// display-list replay (identical semantics, minus the recording check).
void stateEnable( GLenum cap, bool enable );
void stateBindTexture( GLenum target, GLuint texture );
void stateBlendFunc( GLenum sfactor, GLenum dfactor );
void stateLineWidth( GLfloat width );
void stateAlphaFunc( GLenum func, GLclampf ref );
// GL1 normalized-attribute rule: integer colors and normals are normalized,
// floats are not (positions/texcoords are float-only in this codebase).
bool attribNormalized( int arrayIndex, GLenum type );
// Effective byte stride of a client array (GL stride 0 = tightly packed).
GLsizei attribEffectiveStride( GLint size, GLenum type, GLsizei stride );
// One vertex attribute's data source for an array draw: either client memory
// to be uploaded (cpuData) or a byte offset into a user VBO (buffer/offset).
struct AttribSource
{
bool enabled = false;
GLint size = 4;
GLenum type = GL_FLOAT;
bool normalized = false;
GLsizei stride = 0; // effective byte stride, never 0
const void* cpuData = nullptr;
GLuint buffer = 0;
GLintptr offset = 0;
};
// --- matrix module (gl1_matrix.cpp) ---
void matrixLoadIdentity();
void matrixLoadf( const GLfloat* m );
@ -231,7 +260,10 @@ bool immActive();
// Uploads `count` interleaved ImmVertex records to the streaming VBO and draws
// them with the FFP program. `mode` must already be a WebGL2-legal primitive.
void drawImmVertices( GLenum mode, const ImmVertex* verts, GLsizei count );
// Routed glDrawArrays/glDrawElements over FFP client-array state (M3/M5).
// Non-indexed draw over explicit per-attribute sources (client arrays,
// display-list snapshots). Sources are pre-rebased: vertex 0 = first vertex.
void drawArraysWithSources( GLenum mode, GLsizei count, const AttribSource aSrc[CA_COUNT] );
// Routed glDrawArrays/glDrawElements over FFP client-array state.
void drawClientArrays( GLenum mode, GLint first, GLsizei count );
void drawClientElements( GLenum mode, GLsizei count, GLenum type, const GLvoid* indices );

View file

@ -7,22 +7,71 @@
* (layer_triangles.cpp, render_3d_opengl.cpp:1327) glIsList must be
* true right after allocation or nothing ever renders.
* - Only GL_COMPILE recording exists (COMPILE_AND_EXECUTE is asserted).
* - M3 adds the command recorder (state + geometry + glDrawArrays with
* client-array snapshots); until then recorded commands are dropped,
* which keeps the pre-M3 red scenarios blank instead of crashing.
* - THE invariant (do not weaken): a recorded glDrawArrays dereferences the
* client arrays AT COMPILE TIME. The renderer frees them right after
* glEndList (layer_triangles.cpp seg-ends uvArray), so every enabled
* client-memory array is snapshotted eagerly here. VBO-backed pointers
* record the buffer name + offset instead (no copy).
*
* Replay is a literal command replay through the same shim state machine and
* draw pipeline the live calls use state mutations inside a list correctly
* leak into post-glCallList state (GL semantics), and materials changed
* BETWEEN glCallList calls (setLayerMaterial -> DrawAll) are honored because
* draws always read the live uniform state.
*/
#include "gl1_shim.h"
#include <array>
#include <map>
#include <memory>
namespace gl1
{
// Eager copy of one client array's data for a recorded draw (or a reference
// into a user VBO when the pointer was VBO-backed at record time).
struct SnapArray
{
bool enabled = false;
GLint size = 4;
GLenum type = GL_FLOAT;
GLsizei stride = 0; // effective byte stride
std::vector<uint8_t> data; // client-memory snapshot (empty if VBO)
GLuint buffer = 0;
GLintptr offset = 0;
};
struct Cmd
{
enum class Kind : uint8_t
{
ENABLE, // e0 = cap, i0 = on/off
BIND_TEXTURE,// e0 = target, u0 = texture
BLEND_FUNC, // e0 = sfactor, e1 = dfactor
LINE_WIDTH, // f[0]
ALPHA_FUNC, // e0 = func, f[0] = ref
NORMAL, // f[0..2]
COLOR, // f[0..3]
BEGIN, // e0 = mode
VERTEX, // f[0..2]
END,
DRAW_ARRAYS, // e0 = mode, i0 = count, snap
};
Kind kind;
GLenum e0 = 0;
GLenum e1 = 0;
GLuint u0 = 0;
GLint i0 = 0;
float f[4] = {};
std::shared_ptr<std::array<SnapArray, CA_COUNT>> snap;
};
struct DList
{
// M3: recorded command stream + baked static VBO.
bool empty = true;
std::vector<Cmd> cmds;
};
static std::map<GLuint, DList> s_lists;
@ -31,6 +80,13 @@ static bool s_recording = false;
static GLuint s_recordingId = 0;
static std::vector<Cmd>* recCmds()
{
auto it = s_lists.find( s_recordingId );
return it != s_lists.end() ? &it->second.cmds : nullptr;
}
bool dlistRecording()
{
return s_recording;
@ -70,7 +126,7 @@ void dlistNewList( GLuint list, GLenum mode )
if( s_recording )
{
GL1_WARN_ONCE( "glNewList while already recording — previous list discarded" );
GL1_WARN_ONCE( "glNewList while already recording — previous list kept as-is" );
dlistEndList();
}
@ -100,7 +156,85 @@ void dlistCallList( GLuint list )
if( it == s_lists.end() )
return; // calling a nonexistent list is a silent no-op in GL
// M3: replay the recorded command stream.
State& s = S();
for( const Cmd& c : it->second.cmds )
{
switch( c.kind )
{
case Cmd::Kind::ENABLE:
stateEnable( c.e0, c.i0 != 0 );
break;
case Cmd::Kind::BIND_TEXTURE:
stateBindTexture( c.e0, c.u0 );
break;
case Cmd::Kind::BLEND_FUNC:
stateBlendFunc( c.e0, c.e1 );
break;
case Cmd::Kind::LINE_WIDTH:
stateLineWidth( c.f[0] );
break;
case Cmd::Kind::ALPHA_FUNC:
stateAlphaFunc( c.e0, c.f[0] );
break;
case Cmd::Kind::NORMAL:
s.currentNormal = glm::vec3( c.f[0], c.f[1], c.f[2] );
break;
case Cmd::Kind::COLOR:
s.currentColor = glm::vec4( c.f[0], c.f[1], c.f[2], c.f[3] );
break;
case Cmd::Kind::BEGIN:
immBegin( c.e0 );
break;
case Cmd::Kind::VERTEX:
immVertex( c.f[0], c.f[1], c.f[2] );
break;
case Cmd::Kind::END:
immEnd();
break;
case Cmd::Kind::DRAW_ARRAYS:
{
AttribSource src[CA_COUNT];
for( int i = 0; i < CA_COUNT; ++i )
{
const SnapArray& sa = ( *c.snap )[i];
src[i].enabled = sa.enabled;
if( !sa.enabled )
continue;
src[i].size = sa.size;
src[i].type = sa.type;
src[i].normalized = attribNormalized( i, sa.type );
src[i].stride = sa.stride;
if( !sa.data.empty() )
{
src[i].cpuData = sa.data.data();
}
else
{
src[i].buffer = sa.buffer;
src[i].offset = sa.offset;
}
}
drawArraysWithSources( c.e0, c.i0, src );
break;
}
}
}
}
@ -111,92 +245,148 @@ void dlistDeleteLists( GLuint list, GLsizei range )
}
// --- recording hooks (M3 replaces these drops with the command recorder) ---
// --- recording hooks ---
static void push( Cmd&& c )
{
if( std::vector<Cmd>* cmds = recCmds() )
cmds->push_back( std::move( c ) );
}
void dlistRecordEnable( GLenum cap, bool enable )
{
(void) cap;
(void) enable;
GL1_WARN_ONCE( "display-list recorder not implemented yet (M3) — commands dropped" );
Cmd c{ Cmd::Kind::ENABLE };
c.e0 = cap;
c.i0 = enable ? 1 : 0;
push( std::move( c ) );
}
void dlistRecordBindTexture( GLenum target, GLuint texture )
{
(void) target;
(void) texture;
GL1_WARN_ONCE( "display-list recorder not implemented yet (M3) — commands dropped" );
Cmd c{ Cmd::Kind::BIND_TEXTURE };
c.e0 = target;
c.u0 = texture;
push( std::move( c ) );
}
void dlistRecordBlendFunc( GLenum sfactor, GLenum dfactor )
{
(void) sfactor;
(void) dfactor;
GL1_WARN_ONCE( "display-list recorder not implemented yet (M3) — commands dropped" );
Cmd c{ Cmd::Kind::BLEND_FUNC };
c.e0 = sfactor;
c.e1 = dfactor;
push( std::move( c ) );
}
void dlistRecordLineWidth( GLfloat width )
{
(void) width;
GL1_WARN_ONCE( "display-list recorder not implemented yet (M3) — commands dropped" );
Cmd c{ Cmd::Kind::LINE_WIDTH };
c.f[0] = width;
push( std::move( c ) );
}
void dlistRecordAlphaFunc( GLenum func, GLclampf ref )
{
(void) func;
(void) ref;
GL1_WARN_ONCE( "display-list recorder not implemented yet (M3) — commands dropped" );
Cmd c{ Cmd::Kind::ALPHA_FUNC };
c.e0 = func;
c.f[0] = ref;
push( std::move( c ) );
}
void dlistRecordNormal( float nx, float ny, float nz )
{
(void) nx;
(void) ny;
(void) nz;
GL1_WARN_ONCE( "display-list recorder not implemented yet (M3) — commands dropped" );
Cmd c{ Cmd::Kind::NORMAL };
c.f[0] = nx;
c.f[1] = ny;
c.f[2] = nz;
push( std::move( c ) );
}
void dlistRecordColor( float r, float g, float b, float a )
{
(void) r;
(void) g;
(void) b;
(void) a;
GL1_WARN_ONCE( "display-list recorder not implemented yet (M3) — commands dropped" );
Cmd c{ Cmd::Kind::COLOR };
c.f[0] = r;
c.f[1] = g;
c.f[2] = b;
c.f[3] = a;
push( std::move( c ) );
}
void dlistRecordBegin( GLenum mode )
{
(void) mode;
GL1_WARN_ONCE( "display-list recorder not implemented yet (M3) — commands dropped" );
Cmd c{ Cmd::Kind::BEGIN };
c.e0 = mode;
push( std::move( c ) );
}
void dlistRecordVertex( float x, float y, float z )
{
(void) x;
(void) y;
(void) z;
Cmd c{ Cmd::Kind::VERTEX };
c.f[0] = x;
c.f[1] = y;
c.f[2] = z;
push( std::move( c ) );
}
void dlistRecordEnd()
{
push( Cmd{ Cmd::Kind::END } );
}
void dlistRecordDrawArrays( GLenum mode, GLint first, GLsizei count )
{
(void) mode;
(void) first;
(void) count;
GL1_WARN_ONCE( "display-list recorder not implemented yet (M3) — commands dropped" );
if( count <= 0 )
return;
Cmd c{ Cmd::Kind::DRAW_ARRAYS };
c.e0 = mode;
c.i0 = count;
c.snap = std::make_shared<std::array<SnapArray, CA_COUNT>>();
const State& s = S();
for( int i = 0; i < CA_COUNT; ++i )
{
const ClientArray& ca = s.clientArrays[i];
SnapArray& sa = ( *c.snap )[i];
sa.enabled = ca.enabled;
if( !ca.enabled )
continue;
sa.size = ca.size;
sa.type = ca.type;
sa.stride = attribEffectiveStride( ca.size, ca.type, ca.stride );
if( ca.boundBuffer == 0 )
{
// EAGER copy — the caller may (and does) free this memory right
// after glEndList. Whole strided block, last vertex tight-sized.
const GLsizei tight = attribEffectiveStride( ca.size, ca.type, 0 );
const size_t bytes = (size_t) ( count - 1 ) * sa.stride + tight;
const auto* base = (const uint8_t*) ca.pointer + (size_t) first * sa.stride;
sa.data.assign( base, base + bytes );
}
else
{
sa.buffer = ca.boundBuffer;
sa.offset = (GLintptr) ca.pointer + (GLintptr) first * sa.stride;
}
}
push( std::move( c ) );
}
} // namespace gl1

View file

@ -14,10 +14,13 @@
#include "gl1_shim.h"
#include <cstring>
namespace gl1
{
static GLuint s_streamVBO = 0;
static GLuint s_streamVBO = 0; // immediate-mode interleaved stream
static GLuint s_scratchVBO = 0; // client-array upload staging
enum
{
@ -65,24 +68,223 @@ void drawImmVertices( GLenum mode, const ImmVertex* verts, GLsizei count )
}
// Routed glDrawArrays over FFP client-array state (M3).
void drawClientArrays( GLenum mode, GLint first, GLsizei count )
// Constant (current-state) value for a disabled attribute array.
static void setConstantAttrib( int attr )
{
(void) mode;
(void) first;
(void) count;
GL1_WARN_ONCE( "client-array glDrawArrays not implemented yet (M3) — draw dropped" );
const State& s = S();
glDisableVertexAttribArray( attr );
switch( attr )
{
case ATTR_NORMAL:
glVertexAttrib3f( ATTR_NORMAL, s.currentNormal.x, s.currentNormal.y, s.currentNormal.z );
break;
case ATTR_COLOR:
glVertexAttrib4f( ATTR_COLOR, s.currentColor.r, s.currentColor.g, s.currentColor.b,
s.currentColor.a );
break;
case ATTR_TEXCOORD:
glVertexAttrib2f( ATTR_TEXCOORD, 0.0f, 0.0f );
break;
default:
break;
}
}
// Binds all four attributes from the given sources: client-memory sources are
// packed into one scratch-VBO upload; VBO-backed sources bind the user's
// buffer with the given byte offset. Restores GL_ARRAY_BUFFER when done.
static bool setupAttribSources( const AttribSource aSrc[CA_COUNT], GLsizei count )
{
if( !aSrc[CA_VERTEX].enabled )
{
GL1_WARN_ONCE( "array draw without an enabled GL_VERTEX_ARRAY — dropped" );
return false;
}
static std::vector<uint8_t> staging;
staging.clear();
GLintptr cpuOffsets[CA_COUNT] = {};
bool anyCpu = false;
for( int i = 0; i < CA_COUNT; ++i )
{
const AttribSource& src = aSrc[i];
if( !src.enabled || !src.cpuData )
continue;
anyCpu = true;
// Whole strided block; the last vertex only needs its tight size.
const GLsizei tight = attribEffectiveStride( src.size, src.type, 0 );
const size_t bytes = (size_t) ( count - 1 ) * src.stride + tight;
// 4-byte-align each sub-range inside the scratch VBO.
const size_t aligned = ( staging.size() + 3u ) & ~size_t( 3 );
staging.resize( aligned + bytes );
std::memcpy( staging.data() + aligned, src.cpuData, bytes );
cpuOffsets[i] = (GLintptr) aligned;
}
if( anyCpu )
{
if( !s_scratchVBO )
glGenBuffers( 1, &s_scratchVBO );
glBindBuffer( GL_ARRAY_BUFFER, s_scratchVBO );
glBufferData( GL_ARRAY_BUFFER, (GLsizeiptr) staging.size(), staging.data(),
GL_STREAM_DRAW );
}
static const GLint attrOf[CA_COUNT] = { ATTR_POSITION, ATTR_NORMAL, ATTR_COLOR,
ATTR_TEXCOORD };
for( int i = 0; i < CA_COUNT; ++i )
{
const AttribSource& src = aSrc[i];
const GLint attr = attrOf[i];
if( !src.enabled )
{
setConstantAttrib( attr );
continue;
}
if( src.cpuData )
glBindBuffer( GL_ARRAY_BUFFER, s_scratchVBO );
else
glBindBuffer( GL_ARRAY_BUFFER, src.buffer );
glEnableVertexAttribArray( attr );
glVertexAttribPointer( attr, src.size, src.type,
src.normalized ? GL_TRUE : GL_FALSE, src.stride,
(const void*) ( src.cpuData ? cpuOffsets[i] : src.offset ) );
}
return true;
}
void drawArraysWithSources( GLenum mode, GLsizei count, const AttribSource aSrc[CA_COUNT] )
{
if( count <= 0 || !programSync() )
return;
switch( mode )
{
case GL_POINTS:
case GL_LINES:
case GL_LINE_STRIP:
case GL_TRIANGLES:
case GL_TRIANGLE_STRIP:
case GL_TRIANGLE_FAN:
break;
default:
// The renderer only issues array draws with WebGL2-legal primitives
// (GL_TRIANGLES everywhere, GL_LINES for model bboxes).
GL1_WARN_ONCE( "array draw with unsupported primitive 0x%x — dropped", mode );
return;
}
GLint prevArrayBuffer = 0;
glGetIntegerv( GL_ARRAY_BUFFER_BINDING, &prevArrayBuffer );
if( setupAttribSources( aSrc, count ) )
__real_glDrawArrays( mode, 0, count );
glBindBuffer( GL_ARRAY_BUFFER, (GLuint) prevArrayBuffer );
}
// Builds the per-attribute sources from the live client-array state,
// rebasing so that vertex 0 of the draw is `first`.
static void buildLiveSources( AttribSource aOut[CA_COUNT], GLint first )
{
const State& s = S();
for( int i = 0; i < CA_COUNT; ++i )
{
const ClientArray& ca = s.clientArrays[i];
AttribSource& src = aOut[i];
src.enabled = ca.enabled;
if( !ca.enabled )
continue;
src.size = ca.size;
src.type = ca.type;
src.normalized = attribNormalized( i, ca.type );
src.stride = attribEffectiveStride( ca.size, ca.type, ca.stride );
if( ca.boundBuffer == 0 )
{
src.cpuData = (const uint8_t*) ca.pointer + (size_t) first * src.stride;
}
else
{
src.cpuData = nullptr;
src.buffer = ca.boundBuffer;
src.offset = (GLintptr) ca.pointer + (GLintptr) first * src.stride;
}
}
}
void drawClientArrays( GLenum mode, GLint first, GLsizei count )
{
AttribSource src[CA_COUNT];
buildLiveSources( src, first );
drawArraysWithSources( mode, count, src );
}
// Routed glDrawElements over user VBO/IBO state (M5).
void drawClientElements( GLenum mode, GLsizei count, GLenum type, const GLvoid* indices )
{
(void) mode;
(void) count;
(void) type;
(void) indices;
GL1_WARN_ONCE( "client-array glDrawElements not implemented yet (M5) — draw dropped" );
if( count <= 0 || !programSync() )
return;
// The renderer's indexed draws (3d_model.cpp) are always fully VBO-backed:
// vertex attribs offset into a bound GL_ARRAY_BUFFER, indices offset into
// a bound GL_ELEMENT_ARRAY_BUFFER. WebGL2 requires the index buffer.
GLint ibo = 0;
glGetIntegerv( GL_ELEMENT_ARRAY_BUFFER_BINDING, &ibo );
if( ibo == 0 )
{
GL1_WARN_ONCE( "glDrawElements without a bound index buffer — dropped "
"(client-memory indices are not supported)" );
return;
}
AttribSource src[CA_COUNT];
buildLiveSources( src, 0 );
for( int i = 0; i < CA_COUNT; ++i )
{
if( src[i].enabled && src[i].cpuData )
{
GL1_WARN_ONCE( "glDrawElements over client-memory vertex arrays is not supported "
"— dropped" );
return;
}
}
GLint prevArrayBuffer = 0;
glGetIntegerv( GL_ARRAY_BUFFER_BINDING, &prevArrayBuffer );
if( setupAttribSources( src, count ) )
__real_glDrawElements( mode, count, type, indices );
glBindBuffer( GL_ARRAY_BUFFER, (GLuint) prevArrayBuffer );
}
} // namespace gl1

View file

@ -590,9 +590,7 @@ void glAlphaFunc( GLenum func, GLclampf ref )
return;
}
S().alphaFunc = func;
S().alphaRef = ref;
S().miscDirty = true;
stateAlphaFunc( func, ref );
}

View file

@ -32,18 +32,7 @@ void __wrap_glEnable( GLenum cap )
return;
}
if( bool* slot = ffpCapSlot( cap ) )
{
if( !*slot )
{
*slot = true;
onCapChanged( cap );
}
return;
}
__real_glEnable( cap );
stateEnable( cap, true );
}
@ -55,18 +44,7 @@ void __wrap_glDisable( GLenum cap )
return;
}
if( bool* slot = ffpCapSlot( cap ) )
{
if( *slot )
{
*slot = false;
onCapChanged( cap );
}
return;
}
__real_glDisable( cap );
stateEnable( cap, false );
}
@ -144,10 +122,7 @@ void __wrap_glBindTexture( GLenum target, GLuint texture )
return;
}
if( target == GL_TEXTURE_2D )
S().boundTexture2D = texture;
__real_glBindTexture( target, texture );
stateBindTexture( target, texture );
}
@ -159,7 +134,7 @@ void __wrap_glBlendFunc( GLenum sfactor, GLenum dfactor )
return;
}
__real_glBlendFunc( sfactor, dfactor );
stateBlendFunc( sfactor, dfactor );
}
@ -171,8 +146,7 @@ void __wrap_glLineWidth( GLfloat width )
return;
}
S().lineWidth = width;
__real_glLineWidth( width );
stateLineWidth( width );
}

View file

@ -63,4 +63,86 @@ void onCapChanged( GLenum cap )
}
}
void stateEnable( GLenum cap, bool enable )
{
if( bool* slot = ffpCapSlot( cap ) )
{
if( *slot != enable )
{
*slot = enable;
onCapChanged( cap );
}
return;
}
if( enable )
__real_glEnable( cap );
else
__real_glDisable( cap );
}
void stateBindTexture( GLenum target, GLuint texture )
{
if( target == GL_TEXTURE_2D )
S().boundTexture2D = texture;
__real_glBindTexture( target, texture );
}
void stateBlendFunc( GLenum sfactor, GLenum dfactor )
{
__real_glBlendFunc( sfactor, dfactor );
}
void stateLineWidth( GLfloat width )
{
S().lineWidth = width;
__real_glLineWidth( width );
}
void stateAlphaFunc( GLenum func, GLclampf ref )
{
State& s = S();
s.alphaFunc = func;
s.alphaRef = ref;
s.miscDirty = true;
}
bool attribNormalized( int arrayIndex, GLenum type )
{
// GL1 fixed-function semantics: integer color components map to [0,1] and
// integer normals to [-1,1]; float data is used as-is.
if( type == GL_FLOAT )
return false;
return arrayIndex == CA_COLOR || arrayIndex == CA_NORMAL;
}
static GLsizei componentSize( GLenum type )
{
switch( type )
{
case GL_BYTE:
case GL_UNSIGNED_BYTE: return 1;
case GL_SHORT:
case GL_UNSIGNED_SHORT: return 2;
case GL_FLOAT:
default: return 4;
}
}
GLsizei attribEffectiveStride( GLint size, GLenum type, GLsizei stride )
{
return stride != 0 ? stride : size * componentSize( type );
}
} // namespace gl1