mirror of
https://gitlab.freedesktop.org/mesa/mesa.git
synced 2026-05-05 13:58:04 +02:00
intel/decoders: add address space indicator to get BOs
Some commands like MI_BATCH_BUFFER_START have this indicator. Signed-off-by: Lionel Landwerlin <lionel.g.landwerlin@intel.com> Reviewed-by: Rafael Antognolli <rafael.antognolli@intel.com>
This commit is contained in:
parent
3e8d5b5ed4
commit
ec526d6ba0
8 changed files with 65 additions and 42 deletions
|
|
@ -34,6 +34,7 @@ gen_batch_decode_ctx_init(struct gen_batch_decode_ctx *ctx,
|
|||
FILE *fp, enum gen_batch_decode_flags flags,
|
||||
const char *xml_path,
|
||||
struct gen_batch_decode_bo (*get_bo)(void *,
|
||||
bool,
|
||||
uint64_t),
|
||||
unsigned (*get_state_size)(void *, uint32_t),
|
||||
void *user_data)
|
||||
|
|
@ -78,7 +79,7 @@ ctx_print_group(struct gen_batch_decode_ctx *ctx,
|
|||
}
|
||||
|
||||
static struct gen_batch_decode_bo
|
||||
ctx_get_bo(struct gen_batch_decode_ctx *ctx, uint64_t addr)
|
||||
ctx_get_bo(struct gen_batch_decode_ctx *ctx, bool ppgtt, uint64_t addr)
|
||||
{
|
||||
if (gen_spec_get_gen(ctx->spec) >= gen_make_gen(8,0)) {
|
||||
/* On Broadwell and above, we have 48-bit addresses which consume two
|
||||
|
|
@ -90,7 +91,7 @@ ctx_get_bo(struct gen_batch_decode_ctx *ctx, uint64_t addr)
|
|||
addr &= (~0ull >> 16);
|
||||
}
|
||||
|
||||
struct gen_batch_decode_bo bo = ctx->get_bo(ctx->user_data, addr);
|
||||
struct gen_batch_decode_bo bo = ctx->get_bo(ctx->user_data, ppgtt, addr);
|
||||
|
||||
if (gen_spec_get_gen(ctx->spec) >= gen_make_gen(8,0))
|
||||
bo.addr &= (~0ull >> 16);
|
||||
|
|
@ -130,7 +131,7 @@ ctx_disassemble_program(struct gen_batch_decode_ctx *ctx,
|
|||
uint32_t ksp, const char *type)
|
||||
{
|
||||
uint64_t addr = ctx->instruction_base + ksp;
|
||||
struct gen_batch_decode_bo bo = ctx_get_bo(ctx, addr);
|
||||
struct gen_batch_decode_bo bo = ctx_get_bo(ctx, true, addr);
|
||||
if (!bo.map)
|
||||
return;
|
||||
|
||||
|
|
@ -257,7 +258,7 @@ dump_binding_table(struct gen_batch_decode_ctx *ctx, uint32_t offset, int count)
|
|||
}
|
||||
|
||||
struct gen_batch_decode_bo bind_bo =
|
||||
ctx_get_bo(ctx, ctx->surface_base + offset);
|
||||
ctx_get_bo(ctx, true, ctx->surface_base + offset);
|
||||
|
||||
if (bind_bo.map == NULL) {
|
||||
fprintf(ctx->fp, " binding table unavailable\n");
|
||||
|
|
@ -270,7 +271,7 @@ dump_binding_table(struct gen_batch_decode_ctx *ctx, uint32_t offset, int count)
|
|||
continue;
|
||||
|
||||
uint64_t addr = ctx->surface_base + pointers[i];
|
||||
struct gen_batch_decode_bo bo = ctx_get_bo(ctx, addr);
|
||||
struct gen_batch_decode_bo bo = ctx_get_bo(ctx, true, addr);
|
||||
uint32_t size = strct->dw_length * 4;
|
||||
|
||||
if (pointers[i] % 32 != 0 ||
|
||||
|
|
@ -293,7 +294,7 @@ dump_samplers(struct gen_batch_decode_ctx *ctx, uint32_t offset, int count)
|
|||
count = update_count(ctx, offset, strct->dw_length, 4);
|
||||
|
||||
uint64_t state_addr = ctx->dynamic_base + offset;
|
||||
struct gen_batch_decode_bo bo = ctx_get_bo(ctx, state_addr);
|
||||
struct gen_batch_decode_bo bo = ctx_get_bo(ctx, true, state_addr);
|
||||
const void *state_map = bo.map;
|
||||
|
||||
if (state_map == NULL) {
|
||||
|
|
@ -336,7 +337,7 @@ handle_media_interface_descriptor_load(struct gen_batch_decode_ctx *ctx,
|
|||
}
|
||||
|
||||
uint64_t desc_addr = ctx->dynamic_base + descriptor_offset;
|
||||
struct gen_batch_decode_bo bo = ctx_get_bo(ctx, desc_addr);
|
||||
struct gen_batch_decode_bo bo = ctx_get_bo(ctx, true, desc_addr);
|
||||
const void *desc_map = bo.map;
|
||||
|
||||
if (desc_map == NULL) {
|
||||
|
|
@ -405,7 +406,7 @@ handle_3dstate_vertex_buffers(struct gen_batch_decode_ctx *ctx,
|
|||
} else if (strcmp(vbs_iter.name, "Buffer Pitch") == 0) {
|
||||
pitch = vbs_iter.raw_value;
|
||||
} else if (strcmp(vbs_iter.name, "Buffer Starting Address") == 0) {
|
||||
vb = ctx_get_bo(ctx, vbs_iter.raw_value);
|
||||
vb = ctx_get_bo(ctx, true, vbs_iter.raw_value);
|
||||
} else if (strcmp(vbs_iter.name, "Buffer Size") == 0) {
|
||||
vb_size = vbs_iter.raw_value;
|
||||
ready = true;
|
||||
|
|
@ -457,7 +458,7 @@ handle_3dstate_index_buffer(struct gen_batch_decode_ctx *ctx,
|
|||
if (strcmp(iter.name, "Index Format") == 0) {
|
||||
format = iter.raw_value;
|
||||
} else if (strcmp(iter.name, "Buffer Starting Address") == 0) {
|
||||
ib = ctx_get_bo(ctx, iter.raw_value);
|
||||
ib = ctx_get_bo(ctx, true, iter.raw_value);
|
||||
} else if (strcmp(iter.name, "Buffer Size") == 0) {
|
||||
ib_size = iter.raw_value;
|
||||
}
|
||||
|
|
@ -615,7 +616,7 @@ decode_3dstate_constant(struct gen_batch_decode_ctx *ctx, const uint32_t *p)
|
|||
if (read_length[i] == 0)
|
||||
continue;
|
||||
|
||||
struct gen_batch_decode_bo buffer = ctx_get_bo(ctx, read_addr[i]);
|
||||
struct gen_batch_decode_bo buffer = ctx_get_bo(ctx, true, read_addr[i]);
|
||||
if (!buffer.map) {
|
||||
fprintf(ctx->fp, "constant buffer %d unavailable\n", i);
|
||||
continue;
|
||||
|
|
@ -681,7 +682,7 @@ decode_dynamic_state_pointers(struct gen_batch_decode_ctx *ctx,
|
|||
}
|
||||
|
||||
uint64_t state_addr = ctx->dynamic_base + state_offset;
|
||||
struct gen_batch_decode_bo bo = ctx_get_bo(ctx, state_addr);
|
||||
struct gen_batch_decode_bo bo = ctx_get_bo(ctx, true, state_addr);
|
||||
const void *state_map = bo.map;
|
||||
|
||||
if (state_map == NULL) {
|
||||
|
|
@ -864,21 +865,26 @@ gen_print_batch(struct gen_batch_decode_ctx *ctx,
|
|||
}
|
||||
|
||||
if (strcmp(inst_name, "MI_BATCH_BUFFER_START") == 0) {
|
||||
struct gen_batch_decode_bo next_batch = {};
|
||||
uint64_t next_batch_addr;
|
||||
bool ppgtt = false;
|
||||
bool second_level;
|
||||
struct gen_field_iterator iter;
|
||||
gen_field_iterator_init(&iter, inst, p, 0, false);
|
||||
while (gen_field_iterator_next(&iter)) {
|
||||
if (strcmp(iter.name, "Batch Buffer Start Address") == 0) {
|
||||
next_batch = ctx_get_bo(ctx, iter.raw_value);
|
||||
next_batch_addr = iter.raw_value;
|
||||
} else if (strcmp(iter.name, "Second Level Batch Buffer") == 0) {
|
||||
second_level = iter.raw_value;
|
||||
} else if (strcmp(iter.name, "Address Space Indicator") == 0) {
|
||||
ppgtt = iter.raw_value;
|
||||
}
|
||||
}
|
||||
|
||||
struct gen_batch_decode_bo next_batch = ctx_get_bo(ctx, ppgtt, next_batch_addr);
|
||||
|
||||
if (next_batch.map == NULL) {
|
||||
fprintf(ctx->fp, "Secondary batch at 0x%08"PRIx64" unavailable\n",
|
||||
next_batch.addr);
|
||||
next_batch_addr);
|
||||
} else {
|
||||
gen_print_batch(ctx, next_batch.map, next_batch.size,
|
||||
next_batch.addr);
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ struct gen_batch_decode_ctx {
|
|||
* If the given address is inside a buffer, the map pointer should be
|
||||
* offset accordingly so it points at the data corresponding to address.
|
||||
*/
|
||||
struct gen_batch_decode_bo (*get_bo)(void *user_data, uint64_t address);
|
||||
struct gen_batch_decode_bo (*get_bo)(void *user_data, bool ppgtt, uint64_t address);
|
||||
unsigned (*get_state_size)(void *user_data,
|
||||
uint32_t offset_from_dynamic_state_base_addr);
|
||||
void *user_data;
|
||||
|
|
@ -244,6 +244,7 @@ void gen_batch_decode_ctx_init(struct gen_batch_decode_ctx *ctx,
|
|||
FILE *fp, enum gen_batch_decode_flags flags,
|
||||
const char *xml_path,
|
||||
struct gen_batch_decode_bo (*get_bo)(void *,
|
||||
bool,
|
||||
uint64_t),
|
||||
|
||||
unsigned (*get_state_size)(void *, uint32_t),
|
||||
|
|
|
|||
|
|
@ -129,6 +129,15 @@ aubinator_init(void *user_data, int aub_pci_id, const char *app_name)
|
|||
fprintf(outfile, "\n");
|
||||
}
|
||||
|
||||
static struct gen_batch_decode_bo
|
||||
get_bo(void *user_data, bool ppgtt, uint64_t addr)
|
||||
{
|
||||
if (ppgtt)
|
||||
return aub_mem_get_ppgtt_bo(user_data, addr);
|
||||
else
|
||||
return aub_mem_get_ggtt_bo(user_data, addr);
|
||||
}
|
||||
|
||||
static void
|
||||
handle_execlist_write(void *user_data, enum drm_i915_gem_engine_class engine, uint64_t context_descriptor)
|
||||
{
|
||||
|
|
@ -152,11 +161,7 @@ handle_execlist_write(void *user_data, enum drm_i915_gem_engine_class engine, ui
|
|||
assert(ring_bo.size > 0);
|
||||
void *commands = (uint8_t *)ring_bo.map + (ring_buffer_start - ring_bo.addr) + ring_buffer_head;
|
||||
|
||||
if (context_descriptor & 0x100 /* ppgtt */) {
|
||||
batch_ctx.get_bo = aub_mem_get_ppgtt_bo;
|
||||
} else {
|
||||
batch_ctx.get_bo = aub_mem_get_ggtt_bo;
|
||||
}
|
||||
batch_ctx.get_bo = get_bo;
|
||||
|
||||
batch_ctx.engine = engine;
|
||||
gen_print_batch(&batch_ctx, commands,
|
||||
|
|
@ -165,12 +170,18 @@ handle_execlist_write(void *user_data, enum drm_i915_gem_engine_class engine, ui
|
|||
aub_mem_clear_bo_maps(&mem);
|
||||
}
|
||||
|
||||
static struct gen_batch_decode_bo
|
||||
get_legacy_bo(void *user_data, bool ppgtt, uint64_t addr)
|
||||
{
|
||||
return aub_mem_get_ggtt_bo(user_data, addr);
|
||||
}
|
||||
|
||||
static void
|
||||
handle_ring_write(void *user_data, enum drm_i915_gem_engine_class engine,
|
||||
const void *data, uint32_t data_len)
|
||||
{
|
||||
batch_ctx.user_data = &mem;
|
||||
batch_ctx.get_bo = aub_mem_get_ggtt_bo;
|
||||
batch_ctx.get_bo = get_legacy_bo;
|
||||
|
||||
batch_ctx.engine = engine;
|
||||
gen_print_batch(&batch_ctx, data, data_len, 0);
|
||||
|
|
|
|||
|
|
@ -381,7 +381,7 @@ static int ascii85_decode(const char *in, uint32_t **out, bool inflate)
|
|||
}
|
||||
|
||||
static struct gen_batch_decode_bo
|
||||
get_gen_batch_bo(void *user_data, uint64_t address)
|
||||
get_gen_batch_bo(void *user_data, bool ppgtt, uint64_t address)
|
||||
{
|
||||
for (int s = 0; s < num_sections; s++) {
|
||||
if (sections[s].gtt_offset <= address &&
|
||||
|
|
|
|||
|
|
@ -673,11 +673,11 @@ batch_edit_address(void *user_data, uint64_t address, uint32_t len)
|
|||
}
|
||||
|
||||
static struct gen_batch_decode_bo
|
||||
batch_get_bo(void *user_data, uint64_t address)
|
||||
batch_get_bo(void *user_data, bool ppgtt, uint64_t address)
|
||||
{
|
||||
struct batch_window *window = (struct batch_window *) user_data;
|
||||
|
||||
if (window->uses_ppgtt)
|
||||
if (window->uses_ppgtt && ppgtt)
|
||||
return aub_mem_get_ppgtt_bo(&window->mem, address);
|
||||
else
|
||||
return aub_mem_get_ggtt_bo(&window->mem, address);
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ struct aub_decode_urb_stage_state {
|
|||
};
|
||||
|
||||
struct aub_viewer_decode_ctx {
|
||||
struct gen_batch_decode_bo (*get_bo)(void *user_data, uint64_t address);
|
||||
struct gen_batch_decode_bo (*get_bo)(void *user_data, bool ppgtt, uint64_t address);
|
||||
unsigned (*get_state_size)(void *user_data,
|
||||
uint32_t offset_from_dynamic_state_base_addr);
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ void aub_viewer_decode_ctx_init(struct aub_viewer_decode_ctx *ctx,
|
|||
struct aub_viewer_decode_cfg *decode_cfg,
|
||||
struct gen_spec *spec,
|
||||
struct gen_disasm *disasm,
|
||||
struct gen_batch_decode_bo (*get_bo)(void *, uint64_t),
|
||||
struct gen_batch_decode_bo (*get_bo)(void *, bool, uint64_t),
|
||||
unsigned (*get_state_size)(void *, uint32_t),
|
||||
void *user_data);
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ aub_viewer_decode_ctx_init(struct aub_viewer_decode_ctx *ctx,
|
|||
struct aub_viewer_decode_cfg *decode_cfg,
|
||||
struct gen_spec *spec,
|
||||
struct gen_disasm *disasm,
|
||||
struct gen_batch_decode_bo (*get_bo)(void *, uint64_t),
|
||||
struct gen_batch_decode_bo (*get_bo)(void *, bool, uint64_t),
|
||||
unsigned (*get_state_size)(void *, uint32_t),
|
||||
void *user_data)
|
||||
{
|
||||
|
|
@ -92,7 +92,7 @@ aub_viewer_print_group(struct aub_viewer_decode_ctx *ctx,
|
|||
}
|
||||
|
||||
static struct gen_batch_decode_bo
|
||||
ctx_get_bo(struct aub_viewer_decode_ctx *ctx, uint64_t addr)
|
||||
ctx_get_bo(struct aub_viewer_decode_ctx *ctx, bool ppgtt, uint64_t addr)
|
||||
{
|
||||
if (gen_spec_get_gen(ctx->spec) >= gen_make_gen(8,0)) {
|
||||
/* On Broadwell and above, we have 48-bit addresses which consume two
|
||||
|
|
@ -104,7 +104,7 @@ ctx_get_bo(struct aub_viewer_decode_ctx *ctx, uint64_t addr)
|
|||
addr &= (~0ull >> 16);
|
||||
}
|
||||
|
||||
struct gen_batch_decode_bo bo = ctx->get_bo(ctx->user_data, addr);
|
||||
struct gen_batch_decode_bo bo = ctx->get_bo(ctx->user_data, ppgtt, addr);
|
||||
|
||||
if (gen_spec_get_gen(ctx->spec) >= gen_make_gen(8,0))
|
||||
bo.addr &= (~0ull >> 16);
|
||||
|
|
@ -144,7 +144,7 @@ ctx_disassemble_program(struct aub_viewer_decode_ctx *ctx,
|
|||
uint32_t ksp, const char *type)
|
||||
{
|
||||
uint64_t addr = ctx->instruction_base + ksp;
|
||||
struct gen_batch_decode_bo bo = ctx_get_bo(ctx, addr);
|
||||
struct gen_batch_decode_bo bo = ctx_get_bo(ctx, true, addr);
|
||||
if (!bo.map) {
|
||||
ImGui::TextColored(ctx->cfg->missing_color,
|
||||
"Shader unavailable addr=0x%012" PRIx64, addr);
|
||||
|
|
@ -213,7 +213,7 @@ dump_binding_table(struct aub_viewer_decode_ctx *ctx, uint32_t offset, int count
|
|||
}
|
||||
|
||||
struct gen_batch_decode_bo bind_bo =
|
||||
ctx_get_bo(ctx, ctx->surface_base + offset);
|
||||
ctx_get_bo(ctx, true, ctx->surface_base + offset);
|
||||
|
||||
if (bind_bo.map == NULL) {
|
||||
ImGui::TextColored(ctx->cfg->missing_color,
|
||||
|
|
@ -228,7 +228,7 @@ dump_binding_table(struct aub_viewer_decode_ctx *ctx, uint32_t offset, int count
|
|||
continue;
|
||||
|
||||
uint64_t addr = ctx->surface_base + pointers[i];
|
||||
struct gen_batch_decode_bo bo = ctx_get_bo(ctx, addr);
|
||||
struct gen_batch_decode_bo bo = ctx_get_bo(ctx, true, addr);
|
||||
uint32_t size = strct->dw_length * 4;
|
||||
|
||||
if (pointers[i] % 32 != 0 ||
|
||||
|
|
@ -256,7 +256,7 @@ dump_samplers(struct aub_viewer_decode_ctx *ctx, uint32_t offset, int count)
|
|||
count = update_count(ctx, offset, strct->dw_length, 4);
|
||||
|
||||
uint64_t state_addr = ctx->dynamic_base + offset;
|
||||
struct gen_batch_decode_bo bo = ctx_get_bo(ctx, state_addr);
|
||||
struct gen_batch_decode_bo bo = ctx_get_bo(ctx, true, state_addr);
|
||||
const uint8_t *state_map = (const uint8_t *) bo.map;
|
||||
|
||||
if (state_map == NULL) {
|
||||
|
|
@ -303,7 +303,7 @@ handle_media_interface_descriptor_load(struct aub_viewer_decode_ctx *ctx,
|
|||
}
|
||||
|
||||
uint64_t desc_addr = ctx->dynamic_base + descriptor_offset;
|
||||
struct gen_batch_decode_bo bo = ctx_get_bo(ctx, desc_addr);
|
||||
struct gen_batch_decode_bo bo = ctx_get_bo(ctx, true, desc_addr);
|
||||
const uint32_t *desc_map = (const uint32_t *) bo.map;
|
||||
|
||||
if (desc_map == NULL) {
|
||||
|
|
@ -375,7 +375,7 @@ handle_3dstate_vertex_buffers(struct aub_viewer_decode_ctx *ctx,
|
|||
pitch = vbs_iter.raw_value;
|
||||
} else if (strcmp(vbs_iter.name, "Buffer Starting Address") == 0) {
|
||||
buffer_addr = vbs_iter.raw_value;
|
||||
vb = ctx_get_bo(ctx, buffer_addr);
|
||||
vb = ctx_get_bo(ctx, true, buffer_addr);
|
||||
} else if (strcmp(vbs_iter.name, "Buffer Size") == 0) {
|
||||
vb_size = vbs_iter.raw_value;
|
||||
ready = true;
|
||||
|
|
@ -427,7 +427,7 @@ handle_3dstate_index_buffer(struct aub_viewer_decode_ctx *ctx,
|
|||
format = iter.raw_value;
|
||||
} else if (strcmp(iter.name, "Buffer Starting Address") == 0) {
|
||||
buffer_addr = iter.raw_value;
|
||||
ib = ctx_get_bo(ctx, buffer_addr);
|
||||
ib = ctx_get_bo(ctx, true, buffer_addr);
|
||||
} else if (strcmp(iter.name, "Buffer Size") == 0) {
|
||||
ib_size = iter.raw_value;
|
||||
}
|
||||
|
|
@ -578,7 +578,7 @@ decode_3dstate_constant(struct aub_viewer_decode_ctx *ctx,
|
|||
if (read_length[i] == 0)
|
||||
continue;
|
||||
|
||||
struct gen_batch_decode_bo buffer = ctx_get_bo(ctx, read_addr[i]);
|
||||
struct gen_batch_decode_bo buffer = ctx_get_bo(ctx, true, read_addr[i]);
|
||||
if (!buffer.map) {
|
||||
ImGui::TextColored(ctx->cfg->missing_color,
|
||||
"constant buffer %d unavailable addr=0x%012" PRIx64,
|
||||
|
|
@ -650,7 +650,7 @@ decode_dynamic_state_pointers(struct aub_viewer_decode_ctx *ctx,
|
|||
}
|
||||
|
||||
uint64_t state_addr = ctx->dynamic_base + state_offset;
|
||||
struct gen_batch_decode_bo bo = ctx_get_bo(ctx, state_addr);
|
||||
struct gen_batch_decode_bo bo = ctx_get_bo(ctx, true, state_addr);
|
||||
const uint8_t *state_map = (const uint8_t *) bo.map;
|
||||
|
||||
if (state_map == NULL) {
|
||||
|
|
@ -947,22 +947,27 @@ aub_viewer_render_batch(struct aub_viewer_decode_ctx *ctx,
|
|||
}
|
||||
|
||||
if (strcmp(inst_name, "MI_BATCH_BUFFER_START") == 0) {
|
||||
struct gen_batch_decode_bo next_batch = {};
|
||||
uint64_t next_batch_addr;
|
||||
bool ppgtt = false;
|
||||
bool second_level;
|
||||
struct gen_field_iterator iter;
|
||||
gen_field_iterator_init(&iter, inst, p, 0, false);
|
||||
while (gen_field_iterator_next(&iter)) {
|
||||
if (strcmp(iter.name, "Batch Buffer Start Address") == 0) {
|
||||
next_batch = ctx_get_bo(ctx, iter.raw_value);
|
||||
next_batch_addr = iter.raw_value;
|
||||
} else if (strcmp(iter.name, "Second Level Batch Buffer") == 0) {
|
||||
second_level = iter.raw_value;
|
||||
} else if (strcmp(iter.name, "Address Space Indicator") == 0) {
|
||||
ppgtt = iter.raw_value;
|
||||
}
|
||||
}
|
||||
|
||||
struct gen_batch_decode_bo next_batch = ctx_get_bo(ctx, ppgtt, next_batch_addr);
|
||||
|
||||
if (next_batch.map == NULL) {
|
||||
ImGui::TextColored(ctx->cfg->missing_color,
|
||||
"Secondary batch at 0x%012" PRIx64 " unavailable",
|
||||
next_batch.addr);
|
||||
next_batch_addr);
|
||||
} else {
|
||||
aub_viewer_render_batch(ctx, next_batch.map, next_batch.size,
|
||||
next_batch.addr);
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ dump_validation_list(struct intel_batchbuffer *batch)
|
|||
}
|
||||
|
||||
static struct gen_batch_decode_bo
|
||||
decode_get_bo(void *v_brw, uint64_t address)
|
||||
decode_get_bo(void *v_brw, bool ppgtt, uint64_t address)
|
||||
{
|
||||
struct brw_context *brw = v_brw;
|
||||
struct intel_batchbuffer *batch = &brw->batch;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue