Changeset View
Changeset View
Standalone View
Standalone View
head/sys/arm64/arm64/gic_v3.c
Show All 37 Lines | |||||
#include <sys/malloc.h> | #include <sys/malloc.h> | ||||
#include <sys/module.h> | #include <sys/module.h> | ||||
#include <sys/rman.h> | #include <sys/rman.h> | ||||
#include <sys/pcpu.h> | #include <sys/pcpu.h> | ||||
#include <sys/proc.h> | #include <sys/proc.h> | ||||
#include <sys/cpuset.h> | #include <sys/cpuset.h> | ||||
#include <sys/lock.h> | #include <sys/lock.h> | ||||
#include <sys/mutex.h> | #include <sys/mutex.h> | ||||
#include <sys/smp.h> | |||||
#include <vm/vm.h> | #include <vm/vm.h> | ||||
#include <vm/pmap.h> | #include <vm/pmap.h> | ||||
#include <machine/bus.h> | #include <machine/bus.h> | ||||
#include <machine/cpu.h> | #include <machine/cpu.h> | ||||
#include <machine/intr.h> | #include <machine/intr.h> | ||||
#include "pic_if.h" | #include "pic_if.h" | ||||
#include "gic_v3_reg.h" | #include "gic_v3_reg.h" | ||||
#include "gic_v3_var.h" | #include "gic_v3_var.h" | ||||
/* Device and PIC methods */ | /* Device and PIC methods */ | ||||
static void gic_v3_dispatch(device_t, struct trapframe *); | static void gic_v3_dispatch(device_t, struct trapframe *); | ||||
static void gic_v3_eoi(device_t, u_int); | static void gic_v3_eoi(device_t, u_int); | ||||
static void gic_v3_mask_irq(device_t, u_int); | static void gic_v3_mask_irq(device_t, u_int); | ||||
static void gic_v3_unmask_irq(device_t, u_int); | static void gic_v3_unmask_irq(device_t, u_int); | ||||
#ifdef SMP | |||||
static void gic_v3_init_secondary(device_t); | |||||
static void gic_v3_ipi_send(device_t, cpuset_t, u_int); | |||||
#endif | |||||
static device_method_t gic_v3_methods[] = { | static device_method_t gic_v3_methods[] = { | ||||
/* Device interface */ | /* Device interface */ | ||||
DEVMETHOD(device_detach, gic_v3_detach), | DEVMETHOD(device_detach, gic_v3_detach), | ||||
/* PIC interface */ | /* PIC interface */ | ||||
DEVMETHOD(pic_dispatch, gic_v3_dispatch), | DEVMETHOD(pic_dispatch, gic_v3_dispatch), | ||||
DEVMETHOD(pic_eoi, gic_v3_eoi), | DEVMETHOD(pic_eoi, gic_v3_eoi), | ||||
DEVMETHOD(pic_mask, gic_v3_mask_irq), | DEVMETHOD(pic_mask, gic_v3_mask_irq), | ||||
DEVMETHOD(pic_unmask, gic_v3_unmask_irq), | DEVMETHOD(pic_unmask, gic_v3_unmask_irq), | ||||
#ifdef SMP | |||||
DEVMETHOD(pic_init_secondary, gic_v3_init_secondary), | |||||
DEVMETHOD(pic_ipi_send, gic_v3_ipi_send), | |||||
#endif | |||||
/* End */ | /* End */ | ||||
DEVMETHOD_END | DEVMETHOD_END | ||||
}; | }; | ||||
DEFINE_CLASS_0(gic_v3, gic_v3_driver, gic_v3_methods, | DEFINE_CLASS_0(gic_v3, gic_v3_driver, gic_v3_methods, | ||||
sizeof(struct gic_v3_softc)); | sizeof(struct gic_v3_softc)); | ||||
/* | /* | ||||
* Driver-specific definitions. | * Driver-specific definitions. | ||||
*/ | */ | ||||
MALLOC_DEFINE(M_GIC_V3, "GICv3", GIC_V3_DEVSTR); | MALLOC_DEFINE(M_GIC_V3, "GICv3", GIC_V3_DEVSTR); | ||||
/* | /* | ||||
* Helper functions and definitions. | * Helper functions and definitions. | ||||
*/ | */ | ||||
/* Destination registers, either Distributor or Re-Distributor */ | /* Destination registers, either Distributor or Re-Distributor */ | ||||
enum gic_v3_xdist { | enum gic_v3_xdist { | ||||
DIST = 0, | DIST = 0, | ||||
REDIST, | REDIST, | ||||
}; | }; | ||||
/* Helper routines starting with gic_v3_ */ | /* Helper routines starting with gic_v3_ */ | ||||
static int gic_v3_dist_init(struct gic_v3_softc *); | static int gic_v3_dist_init(struct gic_v3_softc *); | ||||
static int gic_v3_redist_alloc(struct gic_v3_softc *); | |||||
static int gic_v3_redist_find(struct gic_v3_softc *); | static int gic_v3_redist_find(struct gic_v3_softc *); | ||||
static int gic_v3_redist_init(struct gic_v3_softc *); | static int gic_v3_redist_init(struct gic_v3_softc *); | ||||
static int gic_v3_cpu_init(struct gic_v3_softc *); | static int gic_v3_cpu_init(struct gic_v3_softc *); | ||||
static void gic_v3_wait_for_rwp(struct gic_v3_softc *, enum gic_v3_xdist); | static void gic_v3_wait_for_rwp(struct gic_v3_softc *, enum gic_v3_xdist); | ||||
/* A sequence of init functions for primary (boot) CPU */ | /* A sequence of init functions for primary (boot) CPU */ | ||||
typedef int (*gic_v3_initseq_t) (struct gic_v3_softc *); | typedef int (*gic_v3_initseq_t) (struct gic_v3_softc *); | ||||
/* Primary CPU initialization sequence */ | /* Primary CPU initialization sequence */ | ||||
static gic_v3_initseq_t gic_v3_primary_init[] = { | static gic_v3_initseq_t gic_v3_primary_init[] = { | ||||
gic_v3_dist_init, | gic_v3_dist_init, | ||||
gic_v3_redist_alloc, | |||||
gic_v3_redist_init, | gic_v3_redist_init, | ||||
gic_v3_cpu_init, | gic_v3_cpu_init, | ||||
NULL | NULL | ||||
}; | }; | ||||
#ifdef SMP | |||||
/* Secondary CPU initialization sequence */ | |||||
static gic_v3_initseq_t gic_v3_secondary_init[] = { | |||||
gic_v3_redist_init, | |||||
gic_v3_cpu_init, | |||||
NULL | |||||
}; | |||||
#endif | |||||
/* | /* | ||||
* Device interface. | * Device interface. | ||||
*/ | */ | ||||
int | int | ||||
gic_v3_attach(device_t dev) | gic_v3_attach(device_t dev) | ||||
{ | { | ||||
struct gic_v3_softc *sc; | struct gic_v3_softc *sc; | ||||
gic_v3_initseq_t *init_func; | gic_v3_initseq_t *init_func; | ||||
▲ Show 20 Lines • Show All 87 Lines • ▼ Show 20 Lines | if (device_is_attached(dev)) { | ||||
* XXX: We should probably deregister PIC | * XXX: We should probably deregister PIC | ||||
*/ | */ | ||||
if (sc->gic_registered) | if (sc->gic_registered) | ||||
panic("Trying to detach registered PIC"); | panic("Trying to detach registered PIC"); | ||||
} | } | ||||
for (rid = 0; rid < (sc->gic_redists.nregions + 1); rid++) | for (rid = 0; rid < (sc->gic_redists.nregions + 1); rid++) | ||||
bus_release_resource(dev, SYS_RES_MEMORY, rid, sc->gic_res[rid]); | bus_release_resource(dev, SYS_RES_MEMORY, rid, sc->gic_res[rid]); | ||||
for (i = 0; i < MAXCPU; i++) | for (i = 0; i < mp_ncpus; i++) | ||||
free(sc->gic_redists.pcpu[i], M_GIC_V3); | free(sc->gic_redists.pcpu[i], M_GIC_V3); | ||||
free(sc->gic_res, M_GIC_V3); | free(sc->gic_res, M_GIC_V3); | ||||
free(sc->gic_redists.regions, M_GIC_V3); | free(sc->gic_redists.regions, M_GIC_V3); | ||||
return (0); | return (0); | ||||
} | } | ||||
Show All 28 Lines | while (1) { | ||||
if (__predict_true((active_irq >= GIC_FIRST_PPI && | if (__predict_true((active_irq >= GIC_FIRST_PPI && | ||||
active_irq <= GIC_LAST_SPI) || active_irq >= GIC_FIRST_LPI)) { | active_irq <= GIC_LAST_SPI) || active_irq >= GIC_FIRST_LPI)) { | ||||
arm_dispatch_intr(active_irq, frame); | arm_dispatch_intr(active_irq, frame); | ||||
continue; | continue; | ||||
} | } | ||||
if (active_irq <= GIC_LAST_SGI) { | if (active_irq <= GIC_LAST_SGI) { | ||||
/* | gic_icc_write(EOIR1, (uint64_t)active_irq); | ||||
* TODO: Implement proper SGI handling. | arm_dispatch_intr(active_irq, frame); | ||||
* Mask it if such is received for some reason. | continue; | ||||
*/ | |||||
device_printf(dev, | |||||
"Received unsupported interrupt type: SGI\n"); | |||||
PIC_MASK(dev, active_irq); | |||||
} | } | ||||
} | } | ||||
} | } | ||||
static void | static void | ||||
gic_v3_eoi(device_t dev, u_int irq) | gic_v3_eoi(device_t dev, u_int irq) | ||||
{ | { | ||||
gic_icc_write(EOIR1, (uint64_t)irq); | gic_icc_write(EOIR1, (uint64_t)irq); | ||||
} | } | ||||
static void | static void | ||||
gic_v3_mask_irq(device_t dev, u_int irq) | gic_v3_mask_irq(device_t dev, u_int irq) | ||||
{ | { | ||||
struct gic_v3_softc *sc; | struct gic_v3_softc *sc; | ||||
sc = device_get_softc(dev); | sc = device_get_softc(dev); | ||||
if (irq >= GIC_FIRST_PPI && irq <= GIC_LAST_PPI) { /* PPIs in corresponding Re-Distributor */ | if (irq <= GIC_LAST_PPI) { /* SGIs and PPIs in corresponding Re-Distributor */ | ||||
gic_r_write(sc, 4, | gic_r_write(sc, 4, | ||||
GICR_SGI_BASE_SIZE + GICD_ICENABLER(irq), GICD_I_MASK(irq)); | GICR_SGI_BASE_SIZE + GICD_ICENABLER(irq), GICD_I_MASK(irq)); | ||||
gic_v3_wait_for_rwp(sc, REDIST); | gic_v3_wait_for_rwp(sc, REDIST); | ||||
} else if (irq >= GIC_FIRST_SPI && irq <= GIC_LAST_SPI) { /* SPIs in distributor */ | } else if (irq >= GIC_FIRST_SPI && irq <= GIC_LAST_SPI) { /* SPIs in distributor */ | ||||
gic_r_write(sc, 4, GICD_ICENABLER(irq), GICD_I_MASK(irq)); | gic_r_write(sc, 4, GICD_ICENABLER(irq), GICD_I_MASK(irq)); | ||||
gic_v3_wait_for_rwp(sc, DIST); | gic_v3_wait_for_rwp(sc, DIST); | ||||
} else if (irq >= GIC_FIRST_LPI) { /* LPIs */ | } else if (irq >= GIC_FIRST_LPI) { /* LPIs */ | ||||
lpi_mask_irq(dev, irq); | lpi_mask_irq(dev, irq); | ||||
} else | } else | ||||
panic("%s: Unsupported IRQ number %u", __func__, irq); | panic("%s: Unsupported IRQ number %u", __func__, irq); | ||||
} | } | ||||
static void | static void | ||||
gic_v3_unmask_irq(device_t dev, u_int irq) | gic_v3_unmask_irq(device_t dev, u_int irq) | ||||
{ | { | ||||
struct gic_v3_softc *sc; | struct gic_v3_softc *sc; | ||||
sc = device_get_softc(dev); | sc = device_get_softc(dev); | ||||
if (irq >= GIC_FIRST_PPI && irq <= GIC_LAST_PPI) { /* PPIs in corresponding Re-Distributor */ | if (irq <= GIC_LAST_PPI) { /* SGIs and PPIs in corresponding Re-Distributor */ | ||||
gic_r_write(sc, 4, | gic_r_write(sc, 4, | ||||
GICR_SGI_BASE_SIZE + GICD_ISENABLER(irq), GICD_I_MASK(irq)); | GICR_SGI_BASE_SIZE + GICD_ISENABLER(irq), GICD_I_MASK(irq)); | ||||
gic_v3_wait_for_rwp(sc, REDIST); | gic_v3_wait_for_rwp(sc, REDIST); | ||||
} else if (irq >= GIC_FIRST_SPI && irq <= GIC_LAST_SPI) { /* SPIs in distributor */ | } else if (irq >= GIC_FIRST_SPI && irq <= GIC_LAST_SPI) { /* SPIs in distributor */ | ||||
gic_d_write(sc, 4, GICD_ISENABLER(irq), GICD_I_MASK(irq)); | gic_d_write(sc, 4, GICD_ISENABLER(irq), GICD_I_MASK(irq)); | ||||
gic_v3_wait_for_rwp(sc, DIST); | gic_v3_wait_for_rwp(sc, DIST); | ||||
} else if (irq >= GIC_FIRST_LPI) { /* LPIs */ | } else if (irq >= GIC_FIRST_LPI) { /* LPIs */ | ||||
lpi_unmask_irq(dev, irq); | lpi_unmask_irq(dev, irq); | ||||
} else | } else | ||||
panic("%s: Unsupported IRQ number %u", __func__, irq); | panic("%s: Unsupported IRQ number %u", __func__, irq); | ||||
} | } | ||||
#ifdef SMP | |||||
static void | |||||
gic_v3_init_secondary(device_t dev) | |||||
{ | |||||
struct gic_v3_softc *sc; | |||||
gic_v3_initseq_t *init_func; | |||||
int err; | |||||
sc = device_get_softc(dev); | |||||
/* Train init sequence for boot CPU */ | |||||
for (init_func = gic_v3_secondary_init; *init_func != NULL; init_func++) { | |||||
err = (*init_func)(sc); | |||||
if (err != 0) { | |||||
device_printf(dev, | |||||
"Could not initialize GIC for CPU%u\n", | |||||
PCPU_GET(cpuid)); | |||||
return; | |||||
} | |||||
} | |||||
/* | /* | ||||
* Try to initialize ITS. | |||||
* If there is no driver attached this routine will fail but that | |||||
* does not mean failure here as only LPIs will not be functional | |||||
* on the current CPU. | |||||
*/ | |||||
if (its_init_cpu(NULL) != 0) { | |||||
device_printf(dev, | |||||
"Could not initialize ITS for CPU%u. " | |||||
"No LPIs will arrive on this CPU\n", | |||||
PCPU_GET(cpuid)); | |||||
} | |||||
/* | |||||
* ARM64TODO: Unmask timer PPIs. To be removed when appropriate | |||||
* mechanism is implemented. | |||||
* Activate the timer interrupts: virtual (27), secure (29), | |||||
* and non-secure (30). Use hardcoded values here as there | |||||
* should be no defines for them. | |||||
*/ | |||||
gic_v3_unmask_irq(dev, 27); | |||||
gic_v3_unmask_irq(dev, 29); | |||||
gic_v3_unmask_irq(dev, 30); | |||||
} | |||||
static void | |||||
gic_v3_ipi_send(device_t dev, cpuset_t cpuset, u_int ipi) | |||||
{ | |||||
u_int cpu; | |||||
uint64_t aff, tlist; | |||||
uint64_t val; | |||||
uint64_t aff_mask; | |||||
/* Set affinity mask to match level 3, 2 and 1 */ | |||||
aff_mask = CPU_AFF1_MASK | CPU_AFF2_MASK | CPU_AFF3_MASK; | |||||
/* Iterate through all CPUs in set */ | |||||
while (!CPU_EMPTY(&cpuset)) { | |||||
aff = tlist = 0; | |||||
for (cpu = 0; cpu < mp_ncpus; cpu++) { | |||||
/* Compose target list for single AFF3:AFF2:AFF1 set */ | |||||
if (CPU_ISSET(cpu, &cpuset)) { | |||||
if (!tlist) { | |||||
/* | |||||
* Save affinity of the first CPU to | |||||
* send IPI to for later comparison. | |||||
*/ | |||||
aff = CPU_AFFINITY(cpu); | |||||
tlist |= (1UL << CPU_AFF0(aff)); | |||||
CPU_CLR(cpu, &cpuset); | |||||
} | |||||
/* Check for same Affinity level 3, 2 and 1 */ | |||||
if ((aff & aff_mask) == (CPU_AFFINITY(cpu) & aff_mask)) { | |||||
tlist |= (1UL << CPU_AFF0(CPU_AFFINITY(cpu))); | |||||
/* Clear CPU in cpuset from target list */ | |||||
CPU_CLR(cpu, &cpuset); | |||||
} | |||||
} | |||||
} | |||||
if (tlist) { | |||||
KASSERT((tlist & ~GICI_SGI_TLIST_MASK) == 0, | |||||
("Target list too long for GICv3 IPI")); | |||||
/* Send SGI to CPUs in target list */ | |||||
val = tlist; | |||||
val |= (uint64_t)CPU_AFF3(aff) << GICI_SGI_AFF3_SHIFT; | |||||
val |= (uint64_t)CPU_AFF2(aff) << GICI_SGI_AFF2_SHIFT; | |||||
val |= (uint64_t)CPU_AFF1(aff) << GICI_SGI_AFF1_SHIFT; | |||||
val |= (uint64_t)(ipi & GICI_SGI_IPI_MASK) << GICI_SGI_IPI_SHIFT; | |||||
gic_icc_write(SGI1R, val); | |||||
} | |||||
} | |||||
} | |||||
#endif | |||||
/* | |||||
* Helper routines | * Helper routines | ||||
*/ | */ | ||||
static void | static void | ||||
gic_v3_wait_for_rwp(struct gic_v3_softc *sc, enum gic_v3_xdist xdist) | gic_v3_wait_for_rwp(struct gic_v3_softc *sc, enum gic_v3_xdist xdist) | ||||
{ | { | ||||
struct resource *res; | struct resource *res; | ||||
u_int cpuid; | u_int cpuid; | ||||
size_t us_left = 1000000; | size_t us_left = 1000000; | ||||
▲ Show 20 Lines • Show All 130 Lines • ▼ Show 20 Lines | gic_v3_dist_init(struct gic_v3_softc *sc) | ||||
for (i = GIC_FIRST_SPI; i < sc->gic_nirqs; i++) | for (i = GIC_FIRST_SPI; i < sc->gic_nirqs; i++) | ||||
gic_d_write(sc, 4, GICD_IROUTER(i), aff); | gic_d_write(sc, 4, GICD_IROUTER(i), aff); | ||||
return (0); | return (0); | ||||
} | } | ||||
/* Re-Distributor */ | /* Re-Distributor */ | ||||
static int | static int | ||||
gic_v3_redist_alloc(struct gic_v3_softc *sc) | |||||
{ | |||||
u_int cpuid; | |||||
/* Allocate struct resource for all CPU's Re-Distributor registers */ | |||||
for (cpuid = 0; cpuid < mp_ncpus; cpuid++) | |||||
if (CPU_ISSET(cpuid, &all_cpus) != 0) | |||||
sc->gic_redists.pcpu[cpuid] = | |||||
malloc(sizeof(*sc->gic_redists.pcpu[0]), | |||||
M_GIC_V3, M_WAITOK); | |||||
else | |||||
sc->gic_redists.pcpu[cpuid] = NULL; | |||||
return (0); | |||||
} | |||||
static int | |||||
gic_v3_redist_find(struct gic_v3_softc *sc) | gic_v3_redist_find(struct gic_v3_softc *sc) | ||||
{ | { | ||||
struct resource r_res; | struct resource r_res; | ||||
bus_space_handle_t r_bsh; | bus_space_handle_t r_bsh; | ||||
uint64_t aff; | uint64_t aff; | ||||
uint64_t typer; | uint64_t typer; | ||||
uint32_t pidr2; | uint32_t pidr2; | ||||
u_int cpuid; | u_int cpuid; | ||||
size_t i; | size_t i; | ||||
cpuid = PCPU_GET(cpuid); | cpuid = PCPU_GET(cpuid); | ||||
/* Allocate struct resource for this CPU's Re-Distributor registers */ | |||||
sc->gic_redists.pcpu[cpuid] = | |||||
malloc(sizeof(*sc->gic_redists.pcpu[0]), M_GIC_V3, M_WAITOK); | |||||
aff = CPU_AFFINITY(cpuid); | aff = CPU_AFFINITY(cpuid); | ||||
/* Affinity in format for comparison with typer */ | /* Affinity in format for comparison with typer */ | ||||
aff = (CPU_AFF3(aff) << 24) | (CPU_AFF2(aff) << 16) | | aff = (CPU_AFF3(aff) << 24) | (CPU_AFF2(aff) << 16) | | ||||
(CPU_AFF1(aff) << 8) | CPU_AFF0(aff); | (CPU_AFF1(aff) << 8) | CPU_AFF0(aff); | ||||
if (bootverbose) { | if (bootverbose) { | ||||
device_printf(sc->dev, | device_printf(sc->dev, | ||||
"Start searching for Re-Distributor\n"); | "Start searching for Re-Distributor\n"); | ||||
} | } | ||||
/* Iterate through Re-Distributor regions */ | /* Iterate through Re-Distributor regions */ | ||||
for (i = 0; i < sc->gic_redists.nregions; i++) { | for (i = 0; i < sc->gic_redists.nregions; i++) { | ||||
/* Take a copy of the region's resource */ | /* Take a copy of the region's resource */ | ||||
r_res = *sc->gic_redists.regions[i]; | r_res = *sc->gic_redists.regions[i]; | ||||
r_bsh = rman_get_bushandle(&r_res); | r_bsh = rman_get_bushandle(&r_res); | ||||
pidr2 = bus_read_4(&r_res, GICR_PIDR2); | pidr2 = bus_read_4(&r_res, GICR_PIDR2); | ||||
switch (pidr2 & GICR_PIDR2_ARCH_MASK) { | switch (pidr2 & GICR_PIDR2_ARCH_MASK) { | ||||
case GICR_PIDR2_ARCH_GICv3: /* fall through */ | case GICR_PIDR2_ARCH_GICv3: /* fall through */ | ||||
case GICR_PIDR2_ARCH_GICv4: | case GICR_PIDR2_ARCH_GICv4: | ||||
break; | break; | ||||
default: | default: | ||||
device_printf(sc->dev, | device_printf(sc->dev, | ||||
"No Re-Distributor found for CPU%u\n", cpuid); | "No Re-Distributor found for CPU%u\n", cpuid); | ||||
free(sc->gic_redists.pcpu[cpuid], M_GIC_V3); | |||||
return (ENODEV); | return (ENODEV); | ||||
} | } | ||||
do { | do { | ||||
typer = bus_read_8(&r_res, GICR_TYPER); | typer = bus_read_8(&r_res, GICR_TYPER); | ||||
if ((typer >> GICR_TYPER_AFF_SHIFT) == aff) { | if ((typer >> GICR_TYPER_AFF_SHIFT) == aff) { | ||||
KASSERT(sc->gic_redists.pcpu[cpuid] != NULL, | KASSERT(sc->gic_redists.pcpu[cpuid] != NULL, | ||||
("Invalid pointer to per-CPU redistributor")); | ("Invalid pointer to per-CPU redistributor")); | ||||
Show All 12 Lines | do { | ||||
r_bsh += | r_bsh += | ||||
(GICR_VLPI_BASE_SIZE + GICR_RESERVED_SIZE); | (GICR_VLPI_BASE_SIZE + GICR_RESERVED_SIZE); | ||||
} | } | ||||
rman_set_bushandle(&r_res, r_bsh); | rman_set_bushandle(&r_res, r_bsh); | ||||
} while ((typer & GICR_TYPER_LAST) == 0); | } while ((typer & GICR_TYPER_LAST) == 0); | ||||
} | } | ||||
free(sc->gic_redists.pcpu[cpuid], M_GIC_V3); | |||||
device_printf(sc->dev, "No Re-Distributor found for CPU%u\n", cpuid); | device_printf(sc->dev, "No Re-Distributor found for CPU%u\n", cpuid); | ||||
return (ENXIO); | return (ENXIO); | ||||
} | } | ||||
static int | static int | ||||
gic_v3_redist_wake(struct gic_v3_softc *sc) | gic_v3_redist_wake(struct gic_v3_softc *sc) | ||||
{ | { | ||||
uint32_t waker; | uint32_t waker; | ||||
▲ Show 20 Lines • Show All 57 Lines • Show Last 20 Lines |