diff --git a/ref/vk/TODO.md b/ref/vk/TODO.md index 13d14138..d64028f4 100644 --- a/ref/vk/TODO.md +++ b/ref/vk/TODO.md @@ -2,7 +2,10 @@ - [x] Emissive waters - [x] add emissive water surface to polygon lights - [x] update emissive color for water surfaces -- [ ] dynamic UVs +- [x] trihash option +- [x] dynamic UVs + - [x] update UVs for conveyors + - [ ] pls don't aggravate validation on changelevel -- cannot reproduce - [ ] discuss integration test strategies # 2023-11-14 E330 diff --git a/ref/vk/shaders/noise.glsl b/ref/vk/shaders/noise.glsl index 2154ccdd..dc41e6af 100644 --- a/ref/vk/shaders/noise.glsl +++ b/ref/vk/shaders/noise.glsl @@ -6,44 +6,40 @@ // xxhash (https://github.com/Cyan4973/xxHash) // From https://www.shadertoy.com/view/Xt3cDn -uint xxhash32(uint p) -{ +uint xxhash32(uint p) { const uint PRIME32_2 = 2246822519U, PRIME32_3 = 3266489917U; const uint PRIME32_4 = 668265263U, PRIME32_5 = 374761393U; uint h32 = p + PRIME32_5; h32 = PRIME32_4*((h32 << 17) | (h32 >> (32 - 17))); - h32 = PRIME32_2*(h32^(h32 >> 15)); - h32 = PRIME32_3*(h32^(h32 >> 13)); - return h32^(h32 >> 16); + h32 = PRIME32_2*(h32^(h32 >> 15)); + h32 = PRIME32_3*(h32^(h32 >> 13)); + return h32^(h32 >> 16); } -uint xxhash32(uvec2 p) -{ - const uint PRIME32_2 = 2246822519U, PRIME32_3 = 3266489917U; +uint xxhash32(uvec2 p) { + const uint PRIME32_2 = 2246822519U, PRIME32_3 = 3266489917U; const uint PRIME32_4 = 668265263U, PRIME32_5 = 374761393U; - uint h32 = p.y + PRIME32_5 + p.x*PRIME32_3; - h32 = PRIME32_4*((h32 << 17) | (h32 >> (32 - 17))); - h32 = PRIME32_2*(h32^(h32 >> 15)); - h32 = PRIME32_3*(h32^(h32 >> 13)); - return h32^(h32 >> 16); + uint h32 = p.y + PRIME32_5 + p.x*PRIME32_3; + h32 = PRIME32_4*((h32 << 17) | (h32 >> (32 - 17))); + h32 = PRIME32_2*(h32^(h32 >> 15)); + h32 = PRIME32_3*(h32^(h32 >> 13)); + return h32^(h32 >> 16); } -uint xxhash32(uvec3 p) -{ - const uint PRIME32_2 = 2246822519U, PRIME32_3 = 3266489917U; +uint xxhash32(uvec3 p) { + const uint PRIME32_2 = 2246822519U, PRIME32_3 = 3266489917U; const uint PRIME32_4 = 668265263U, PRIME32_5 = 374761393U; uint h32 = p.z + PRIME32_5 + p.x*PRIME32_3; h32 = PRIME32_4*((h32 << 17) | (h32 >> (32 - 17))); h32 += p.y * PRIME32_3; h32 = PRIME32_4*((h32 << 17) | (h32 >> (32 - 17))); - h32 = PRIME32_2*(h32^(h32 >> 15)); - h32 = PRIME32_3*(h32^(h32 >> 13)); - return h32^(h32 >> 16); + h32 = PRIME32_2*(h32^(h32 >> 15)); + h32 = PRIME32_3*(h32^(h32 >> 13)); + return h32^(h32 >> 16); } -uint xxhash32(uvec4 p) -{ - const uint PRIME32_2 = 2246822519U, PRIME32_3 = 3266489917U; +uint xxhash32(uvec4 p) { + const uint PRIME32_2 = 2246822519U, PRIME32_3 = 3266489917U; const uint PRIME32_4 = 668265263U, PRIME32_5 = 374761393U; uint h32 = p.w + PRIME32_5 + p.x*PRIME32_3; h32 = PRIME32_4*((h32 << 17) | (h32 >> (32 - 17))); @@ -51,90 +47,85 @@ uint xxhash32(uvec4 p) h32 = PRIME32_4*((h32 << 17) | (h32 >> (32 - 17))); h32 += p.z * PRIME32_3; h32 = PRIME32_4*((h32 << 17) | (h32 >> (32 - 17))); - h32 = PRIME32_2*(h32^(h32 >> 15)); - h32 = PRIME32_3*(h32^(h32 >> 13)); - return h32^(h32 >> 16); + h32 = PRIME32_2*(h32^(h32 >> 15)); + h32 = PRIME32_3*(h32^(h32 >> 13)); + return h32^(h32 >> 16); } // https://www.pcg-random.org/ -uint pcg(uint v) -{ +uint pcg(uint v) { uint state = v * 747796405u + 2891336453u; uint word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u; return (word >> 22u) ^ word; } -uvec2 pcg2d(uvec2 v) -{ - v = v * 1664525u + 1013904223u; +uvec2 pcg2d(uvec2 v) { + v = v * 1664525u + 1013904223u; - v.x += v.y * 1664525u; - v.y += v.x * 1664525u; + v.x += v.y * 1664525u; + v.y += v.x * 1664525u; - v = v ^ (v>>16u); + v = v ^ (v>>16u); - v.x += v.y * 1664525u; - v.y += v.x * 1664525u; + v.x += v.y * 1664525u; + v.y += v.x * 1664525u; - v = v ^ (v>>16u); + v = v ^ (v>>16u); - return v; + return v; } // http://www.jcgt.org/published/0009/03/02/ uvec3 pcg3d(uvec3 v) { + v = v * 1664525u + 1013904223u; - v = v * 1664525u + 1013904223u; + v.x += v.y*v.z; + v.y += v.z*v.x; + v.z += v.x*v.y; - v.x += v.y*v.z; - v.y += v.z*v.x; - v.z += v.x*v.y; + v ^= v >> 16u; - v ^= v >> 16u; + v.x += v.y*v.z; + v.y += v.z*v.x; + v.z += v.x*v.y; - v.x += v.y*v.z; - v.y += v.z*v.x; - v.z += v.x*v.y; - - return v; + return v; } // http://www.jcgt.org/published/0009/03/02/ -uvec3 pcg3d16(uvec3 v) -{ - v = v * 12829u + 47989u; +uvec3 pcg3d16(uvec3 v) { + v = v * 12829u + 47989u; - v.x += v.y*v.z; - v.y += v.z*v.x; - v.z += v.x*v.y; + v.x += v.y*v.z; + v.y += v.z*v.x; + v.z += v.x*v.y; - v.x += v.y*v.z; - v.y += v.z*v.x; - v.z += v.x*v.y; + v.x += v.y*v.z; + v.y += v.z*v.x; + v.z += v.x*v.y; v >>= 16u; - return v; + return v; } // http://www.jcgt.org/published/0009/03/02/ -uvec4 pcg4d(uvec4 v) -{ - v = v * 1664525u + 1013904223u; +uvec4 pcg4d(uvec4 v) { + v = v * 1664525u + 1013904223u; - v.x += v.y*v.w; - v.y += v.z*v.x; - v.z += v.x*v.y; - v.w += v.y*v.z; + v.x += v.y*v.w; + v.y += v.z*v.x; + v.z += v.x*v.y; + v.w += v.y*v.z; - v ^= v >> 16u; + v ^= v >> 16u; - v.x += v.y*v.w; - v.y += v.z*v.x; - v.z += v.x*v.y; - v.w += v.y*v.z; + v.x += v.y*v.w; + v.y += v.z*v.x; + v.z += v.x*v.y; + v.w += v.y*v.z; - return v; + return v; } uint rand01_state = 0; @@ -154,7 +145,7 @@ float rand01() { } vec3 rand3_f01(uvec3 seed) { - uvec3 v = pcg3d(seed); - return vec3(uintToFloat01(v.x), uintToFloat01(v.y), uintToFloat01(v.z)); + uvec3 v = pcg3d(seed); + return vec3(uintToFloat01(v.x), uintToFloat01(v.y), uintToFloat01(v.z)); } #endif // NOISE_GLSL_INCLUDED diff --git a/ref/vk/shaders/ray_interop.h b/ref/vk/shaders/ray_interop.h index 0fd0e740..666261f8 100644 --- a/ref/vk/shaders/ray_interop.h +++ b/ref/vk/shaders/ray_interop.h @@ -180,6 +180,7 @@ struct PushConstants { #define DEBUG_DISPLAY_INDIRECT 9 #define DEBUG_DISPLAY_INDIRECT_SPEC 10 #define DEBUG_DISPLAY_INDIRECT_DIFF 11 +#define DEBUG_DISPLAY_TRIHASH 12 // add more when needed struct UniformBuffer { diff --git a/ref/vk/shaders/ray_primary_hit.glsl b/ref/vk/shaders/ray_primary_hit.glsl index f0bc5f3e..5b0c0cc7 100644 --- a/ref/vk/shaders/ray_primary_hit.glsl +++ b/ref/vk/shaders/ray_primary_hit.glsl @@ -106,9 +106,16 @@ void primaryRayHit(rayQueryEXT rq, inout RayPayloadPrimary payload) { payload.base_color_a *= color; payload.emissive.rgb *= color.rgb; - if (ubo.ubo.debug_display_only == DEBUG_DISPLAY_SURFHASH) { + + if (ubo.ubo.debug_display_only == DEBUG_DISPLAY_DISABLED) { + // Nop + } else if (ubo.ubo.debug_display_only == DEBUG_DISPLAY_SURFHASH) { const uint hash = xxhash32(geom.kusok_index); payload.emissive.rgb = vec3(0xff & (hash>>16), 0xff & (hash>>8), 0xff & hash) / 255.; + } else if (ubo.ubo.debug_display_only == DEBUG_DISPLAY_TRIHASH) { + const int primitive_index = rayQueryGetIntersectionPrimitiveIndexEXT(rq, true); + const uint hash = xxhash32(geom.kusok_index + primitive_index * 2246822519U); + payload.emissive.rgb = vec3(0xff & (hash>>16), 0xff & (hash>>8), 0xff & hash) / 255.; } } diff --git a/ref/vk/vk_brush.c b/ref/vk/vk_brush.c index 61484583..88b2c98e 100644 --- a/ref/vk/vk_brush.c +++ b/ref/vk/vk_brush.c @@ -36,6 +36,14 @@ typedef struct { vk_render_model_t render_model; } r_brush_water_model_t; +typedef struct { + float texture_width; + int vertices_count; + int vertices_src_offset; + int vertices_dst_offset; + int geometry_index; +} r_conveyor_t; + typedef struct vk_brush_model_s { model_t *engine_model; @@ -52,6 +60,10 @@ typedef struct vk_brush_model_s { r_brush_water_model_t water; r_brush_water_model_t water_sides; + + int conveyors_count; + r_conveyor_t *conveyors; + vk_vertex_t *conveyors_vertices; } vk_brush_model_t; typedef struct { @@ -64,6 +76,8 @@ typedef struct { int num_surfaces, num_vertices, num_indices; int max_texture_id; int animated_count; + int conveyors_count; + int conveyors_vertices_count; water_model_sizes_t water, side_water; } model_sizes_t; @@ -547,6 +561,7 @@ typedef enum { BrushSurface_Water, BrushSurface_WaterSide, BrushSurface_Sky, + BrushSurface_Conveyor, } brush_surface_type_e; static brush_surface_type_e getSurfaceType( const msurface_t *surf, int i, qboolean is_worldmodel ) { @@ -595,9 +610,12 @@ static brush_surface_type_e getSurfaceType( const msurface_t *surf, int i, qbool if( FBitSet( surf->flags, SURF_DRAWSKY )) return BrushSurface_Sky; + if( surf->flags & SURF_CONVEYOR ) { + return BrushSurface_Conveyor; + } + //if( surf->flags & ( SURF_DRAWSKY | SURF_DRAWTURB | SURF_CONVEYOR | SURF_DRAWTURB_QUADS ) ) { if( surf->flags & ( SURF_DRAWTURB | SURF_DRAWTURB_QUADS ) ) { - //if( surf->flags & ( SURF_DRAWSKY | SURF_CONVEYOR ) ) { // FIXME don't print this on second sort-by-texture pass //DEBUG("Skipping surface %d because of flags %08x", i, surf->flags); return BrushSurface_Hidden; @@ -723,16 +741,12 @@ static void brushDrawWater(r_brush_water_model_t *wmodel, const cl_entity_t *ent APROF_SCOPE_END(brush_draw_water); } -#if 0 -// TODO use this -static void computeConveyorSpeed(const color24 rendercolor, int tex_index, vec2_t speed) { +static void computeConveyorOffset(const color24 rendercolor, float tex_width, float time, vec2_t out_offset) { float sy, cy; float flConveyorSpeed = 0.0f; float flRate, flAngle; - vk_texture_t *texture = R_TextureGetByIndex( tex_index ); - //gl_texture_t *texture; - // FIXME + // TODO /* if( ENGINE_GET_PARM( PARM_QUAKE_COMPATIBLE ) && RI.currententity == gEngfuncs.GetEntityByIndex( 0 ) ) */ /* { */ /* // same as doom speed */ @@ -743,16 +757,23 @@ static void computeConveyorSpeed(const color24 rendercolor, int tex_index, vec2_ flConveyorSpeed = (rendercolor.g<<8|rendercolor.b) / 16.0f; if( rendercolor.r ) flConveyorSpeed = -flConveyorSpeed; } - //texture = R_GetTexture( glState.currentTextures[glState.activeTMU] ); - flRate = fabs( flConveyorSpeed ) / (float)texture->width; + flRate = fabs( flConveyorSpeed ) / tex_width; flAngle = ( flConveyorSpeed >= 0 ) ? 180 : 0; + // TODO no SinCos, no SinCos( flAngle * ( M_PI_F / 180.0f ), &sy, &cy ); - speed[0] = cy * flRate; - speed[1] = sy * flRate; + out_offset[0] = cy * flRate * time; + out_offset[1] = sy * flRate * time; + + // make sure that we are positive + if( out_offset[0] < 0.0f ) out_offset[0] += 1.0f + -(int)out_offset[0]; + if( out_offset[1] < 0.0f ) out_offset[1] += 1.0f + -(int)out_offset[1]; + + // make sure that we are in a [0,1] range + out_offset[0] = out_offset[0] - (int)out_offset[0]; + out_offset[1] = out_offset[1] - (int)out_offset[1]; } -#endif /* =============== @@ -873,7 +894,6 @@ void R_BrushModelDraw( const cl_entity_t *ent, int render_mode, float blend, con brushDrawWater(&bmodel->water_sides, ent, bmodel->engine_model->surfaces, render_type, color, transform, bmodel->prev_transform, bmodel->prev_time); } - ++g_brush.stat.models_drawn; if (bmodel->render_model.num_geometries == 0) @@ -919,6 +939,28 @@ void R_BrushModelDraw( const cl_entity_t *ent, int render_mode, float blend, con APROF_SCOPE_END(brush_update_textures); } + // Move conveyors + for (int i = 0; i < bmodel->conveyors_count; ++i) { + const r_conveyor_t *const conv = bmodel->conveyors + i; + vec2_t offset = {0, 0}; + computeConveyorOffset(ent->curstate.rendercolor, conv->texture_width, gpGlobals->time, offset); + + ASSERT(conv->geometry_index >= 0); + ASSERT(conv->geometry_index < bmodel->render_model.num_geometries); + const vk_render_geometry_t *const geom = bmodel->render_model.geometries + conv->geometry_index; + const r_geometry_range_lock_t lock = R_GeometryRangeLockSubrange(&bmodel->geometry, conv->vertices_dst_offset, conv->vertices_count); + + for (int j = 0; j < conv->vertices_count; ++j) { + const vk_vertex_t *const src = bmodel->conveyors_vertices + conv->vertices_src_offset + j; + vk_vertex_t *const dst = lock.vertices + j; + *dst = *src; + dst->gl_tc[0] = src->gl_tc[0] + offset[0]; + dst->gl_tc[1] = src->gl_tc[1] + offset[1]; + } + + R_GeometryRangeUnlock(&lock); + } + const material_mode_e material_mode = brushMaterialModeForRenderType(render_type); R_RenderModelDraw(&bmodel->render_model, (r_model_draw_t){ .render_type = render_type, @@ -958,6 +1000,11 @@ static model_sizes_t computeSizes( const model_t *mod, qboolean is_worldmodel ) case BrushSurface_Animated: sizes.animated_count++; + break; + case BrushSurface_Conveyor: + sizes.conveyors_count++; + sizes.conveyors_vertices_count += surf->numedges; + break; case BrushSurface_Regular: case BrushSurface_Sky: break; @@ -971,9 +1018,10 @@ static model_sizes_t computeSizes( const model_t *mod, qboolean is_worldmodel ) DEBUG("Computed sizes for brush model \"%s\":", mod->name); DEBUG(" num_surfaces=%d animated_count=%d num_vertices=%d num_indices=%d max_texture_id=%d", sizes.num_surfaces, sizes.animated_count, sizes.num_vertices, sizes.num_indices, sizes.max_texture_id); + DEBUG(" conveyors_count=%d conveyors_vertices_count=%d", + sizes.conveyors_count, sizes.conveyors_vertices_count); DEBUG(" water_surfaces=%d water_vertices=%d water_indices=%d", sizes.water.surfaces, sizes.water.vertices, sizes.water.indices); - DEBUG(" side_water_surfaces=%d side_water_vertices=%d side_water_indices=%d", sizes.side_water.surfaces, sizes.side_water.vertices, sizes.side_water.indices); @@ -1234,6 +1282,8 @@ static qboolean fillBrushSurfaces(fill_geometries_args_t args) { int vertex_offset = 0; int num_geometries = 0; int animated_count = 0; + int conveyors_count = 0; + int conveyors_vertices_count = 0; vk_vertex_t *p_vert = args.out_vertices; uint16_t *p_ind = args.out_indices; @@ -1270,6 +1320,9 @@ static qboolean fillBrushSurfaces(fill_geometries_args_t args) { continue; case BrushSurface_Animated: args.bmodel->animated_indexes[animated_count++] = num_geometries; + break; + case BrushSurface_Conveyor: + break; case BrushSurface_Regular: case BrushSurface_Sky: break; @@ -1277,6 +1330,24 @@ static qboolean fillBrushSurfaces(fill_geometries_args_t args) { args.bmodel->surface_to_geometry_index[i] = num_geometries; + // Fill conveyor data if conveyor + r_conveyor_t *conv = NULL; + if (type == BrushSurface_Conveyor) { + ASSERT(conveyors_count < args.sizes.conveyors_count); + conv = &args.bmodel->conveyors[conveyors_count++]; + + conv->vertices_count = surf->numedges; + + conv->vertices_dst_offset = vertex_offset; + conv->vertices_src_offset = conveyors_vertices_count; + conveyors_vertices_count += conv->vertices_count; + ASSERT(conveyors_vertices_count <= args.sizes.conveyors_vertices_count); + + conv->geometry_index = num_geometries; + + conv->texture_width = R_TexturesGetParm(PARM_TEX_WIDTH, orig_tex_id); + } + ++num_geometries; //DEBUG( "surface %d: numverts=%d numedges=%d", i, surf->polys ? surf->polys->numverts : -1, surf->numedges ); @@ -1337,13 +1408,10 @@ static qboolean fillBrushSurfaces(fill_geometries_args_t args) { VK_CreateSurfaceLightmap( surf, args.mod ); } - if (FBitSet( surf->flags, SURF_CONVEYOR )) { - // FIXME make an explicit list of dynamic-uv geometries - } - vec3_t surf_normal; getSurfaceNormal(surf, surf_normal); + vk_vertex_t *const pvert_begin = p_vert; for( int k = 0; k < surf->numedges; k++ ) { @@ -1425,6 +1493,13 @@ static qboolean fillBrushSurfaces(fill_geometries_args_t args) { Vector4Set(vertex.color, 255, 255, 255, 255); + // Store original vertex data for conveyor reasons + if (conv) { + const int vertex_index = conv->vertices_src_offset + k; + ASSERT(vertex_index < args.sizes.conveyors_vertices_count); + args.bmodel->conveyors_vertices[vertex_index] = vertex; + } + *(p_vert++) = vertex; // Ray tracing apparently expects triangle list only (although spec is not very clear about this kekw) @@ -1444,6 +1519,8 @@ static qboolean fillBrushSurfaces(fill_geometries_args_t args) { ASSERT(args.sizes.num_surfaces == num_geometries); ASSERT(args.sizes.animated_count == animated_count); + ASSERT(args.sizes.conveyors_count == conveyors_count); + ASSERT(args.sizes.conveyors_vertices_count == conveyors_vertices_count); return true; } @@ -1465,6 +1542,13 @@ static qboolean createRenderModel( const model_t *mod, vk_brush_model_t *bmodel, WARN("Too many animated textures %d for model \"%s\" some surfaces can be static", sizes.animated_count, mod->name); } + if (sizes.conveyors_count > 0) { + ASSERT(sizes.conveyors_vertices_count > 3); + bmodel->conveyors_count = sizes.conveyors_count; + bmodel->conveyors_vertices = Mem_Malloc(vk_core.pool, sizeof(vk_vertex_t) * sizes.conveyors_vertices_count); + bmodel->conveyors = Mem_Malloc(vk_core.pool, sizeof(r_conveyor_t) * sizes.conveyors_count); + } + const r_geometry_range_lock_t geom_lock = R_GeometryRangeLock(&bmodel->geometry); const qboolean fill_result = fillBrushSurfaces((fill_geometries_args_t){ @@ -1564,6 +1648,12 @@ static void R_BrushModelDestroy( vk_brush_model_t *bmodel ) { ASSERT(bmodel->engine_model->cache.data == bmodel); ASSERT(bmodel->engine_model->type == mod_brush); + if (bmodel->conveyors_vertices) + Mem_Free(bmodel->conveyors_vertices); + + if (bmodel->conveyors) + Mem_Free(bmodel->conveyors); + if (bmodel->water.surfaces_count) { R_RenderModelDestroy(&bmodel->water.render_model); Mem_Free((int*)bmodel->water.surfaces_indices); diff --git a/ref/vk/vk_geometry.c b/ref/vk/vk_geometry.c index 3ba861ea..d9725381 100644 --- a/ref/vk/vk_geometry.c +++ b/ref/vk/vk_geometry.c @@ -83,6 +83,31 @@ r_geometry_range_lock_t R_GeometryRangeLock(const r_geometry_range_t *range) { }; } +r_geometry_range_lock_t R_GeometryRangeLockSubrange(const r_geometry_range_t *range, int vertices_offset, int vertices_count ) { + const vk_staging_buffer_args_t staging_args = { + .buffer = g_geom.buffer.buffer, + .offset = range->block_handle.offset + sizeof(vk_vertex_t) * vertices_offset, + .size = sizeof(vk_vertex_t) * vertices_count, + .alignment = 4, + }; + + ASSERT(staging_args.offset >= range->block_handle.offset); + ASSERT(staging_args.offset + staging_args.size <= range->block_handle.offset + range->block_handle.size); + + const vk_staging_region_t staging = R_VkStagingLockForBuffer(staging_args); + ASSERT(staging.ptr); + + ASSERT( range->block_handle.offset % sizeof(vk_vertex_t) == 0 ); + + return (r_geometry_range_lock_t){ + .vertices = (vk_vertex_t *)staging.ptr, + .indices = NULL, + .impl_ = { + .staging_handle = staging.handle, + }, + }; +} + void R_GeometryRangeUnlock(const r_geometry_range_lock_t *lock) { R_VkStagingUnlock(lock->impl_.staging_handle); } diff --git a/ref/vk/vk_geometry.h b/ref/vk/vk_geometry.h index 2ba36165..70637988 100644 --- a/ref/vk/vk_geometry.h +++ b/ref/vk/vk_geometry.h @@ -52,6 +52,7 @@ typedef struct { // Lock staging memory for uploading r_geometry_range_lock_t R_GeometryRangeLock(const r_geometry_range_t *range); +r_geometry_range_lock_t R_GeometryRangeLockSubrange(const r_geometry_range_t *range, int vertices_offset, int vertices_count ); void R_GeometryRangeUnlock(const r_geometry_range_lock_t *lock); typedef struct { diff --git a/ref/vk/vk_rtx.c b/ref/vk/vk_rtx.c index d634865b..37e811c9 100644 --- a/ref/vk/vk_rtx.c +++ b/ref/vk/vk_rtx.c @@ -164,6 +164,7 @@ static void parseDebugDisplayValue( void ) { X(NSHADE) \ X(NGEOM) \ X(SURFHASH) \ + X(TRIHASH) \ X(DIRECT) \ X(INDIRECT) \ X(INDIRECT_SPEC) \