On 17-01-04 20:42:32, Ville Syrjälä wrote: >From: Ville Syrjälä <ville.syrjala at linux.intel.com> > >SKL+ display engine can scan out certain kinds of compressed surfaces >produced by the render engine. This involved telling the display engine >the location of the color control surfae (CCS) which describes >which parts of the main surface are compressed and which are not. The >location of CCS is provided by userspace as just another plane with its >own offset. > >Add the required stuff to validate the user provided AUX plane metadata >and convert the user provided linear offset into something the hardware >can consume. > >Due to hardware limitations we require that the main surface and >the AUX surface (CCS) be part of the same bo. The hardware also >makes life hard by not allowing you to provide separate x/y offsets >for the main and AUX surfaces (excpet with NV12), so finding suitable >offsets for both requires a bit of work. Assuming we still want keep >playing tricks with the offsets. I've just gone with a dumb "search >backward for suitable offsets" approach, which is far from optimal, >but it works. > >Also not all planes will be capable of scanning out compressed surfaces, >and eg. 90/270 degree rotation is not supported in combination with >decompression either. > >This patch may contain work from at least the following people: >* Vandana Kannan <vandana.kannan at intel.com> >* Daniel Vetter <daniel at ffwll.ch> >* Ben Widawsky <ben at bwidawsk.net> > >Cc: Vandana Kannan <vandana.kannan at intel.com> >Cc: Daniel Vetter <daniel at ffwll.ch> >Cc: Ben Widawsky <ben at bwidawsk.net> >Cc: Jason Ekstrand <jason at jlekstrand.net> >Signed-off-by: Ville Syrjälä <ville.syrjala at linux.intel.com> >--- > drivers/gpu/drm/i915/i915_reg.h | 22 ++++ > drivers/gpu/drm/i915/intel_display.c | 219 +++++++++++++++++++++++++++++++++-- > drivers/gpu/drm/i915/intel_pm.c | 8 +- > drivers/gpu/drm/i915/intel_sprite.c | 5 + > 4 files changed, 240 insertions(+), 14 deletions(-) > >diff --git a/drivers/gpu/drm/i915/i915_reg.h b/drivers/gpu/drm/i915/i915_reg.h >index 00970aa77afa..05e18e742776 100644 >--- a/drivers/gpu/drm/i915/i915_reg.h >+++ b/drivers/gpu/drm/i915/i915_reg.h >@@ -6209,6 +6209,28 @@ enum { > _ID(id, _PS_ECC_STAT_1A, _PS_ECC_STAT_2A), \ > _ID(id, _PS_ECC_STAT_1B, _PS_ECC_STAT_2B)) > >+#define PLANE_AUX_DIST_1_A 0x701c0 >+#define PLANE_AUX_DIST_2_A 0x702c0 >+#define PLANE_AUX_DIST_1_B 0x711c0 >+#define PLANE_AUX_DIST_2_B 0x712c0 >+#define _PLANE_AUX_DIST_1(pipe) \ >+ _PIPE(pipe, PLANE_AUX_DIST_1_A, PLANE_AUX_DIST_1_B) >+#define _PLANE_AUX_DIST_2(pipe) \ >+ _PIPE(pipe, PLANE_AUX_DIST_2_A, PLANE_AUX_DIST_2_B) >+#define PLANE_AUX_DIST(pipe, plane) \ >+ _MMIO_PLANE(plane, _PLANE_AUX_DIST_1(pipe), _PLANE_AUX_DIST_2(pipe)) >+ >+#define PLANE_AUX_OFFSET_1_A 0x701c4 >+#define PLANE_AUX_OFFSET_2_A 0x702c4 >+#define PLANE_AUX_OFFSET_1_B 0x711c4 >+#define PLANE_AUX_OFFSET_2_B 0x712c4 >+#define _PLANE_AUX_OFFSET_1(pipe) \ >+ _PIPE(pipe, PLANE_AUX_OFFSET_1_A, PLANE_AUX_OFFSET_1_B) >+#define _PLANE_AUX_OFFSET_2(pipe) \ >+ _PIPE(pipe, PLANE_AUX_OFFSET_2_A, PLANE_AUX_OFFSET_2_B) >+#define PLANE_AUX_OFFSET(pipe, plane) \ >+ _MMIO_PLANE(plane, _PLANE_AUX_OFFSET_1(pipe), _PLANE_AUX_OFFSET_2(pipe)) >+ > /* legacy palette */ > #define _LGC_PALETTE_A 0x4a000 > #define _LGC_PALETTE_B 0x4a800 >diff --git a/drivers/gpu/drm/i915/intel_display.c >b/drivers/gpu/drm/i915/intel_display.c >index 38de9df0ec60..b547332eeda1 100644 >--- a/drivers/gpu/drm/i915/intel_display.c >+++ b/drivers/gpu/drm/i915/intel_display.c >@@ -2064,11 +2064,19 @@ intel_tile_width_bytes(const struct drm_framebuffer >*fb, int plane) > return 128; > else > return 512; >+ case I915_FORMAT_MOD_Y_TILED_CCS: >+ if (plane == 1) >+ return 64; >+ /* fall through */ > case I915_FORMAT_MOD_Y_TILED: > if (IS_GEN2(dev_priv) || HAS_128_BYTE_Y_TILING(dev_priv)) > return 128; > else > return 512; >+ case I915_FORMAT_MOD_Yf_TILED_CCS: >+ if (plane == 1) >+ return 64; >+ /* fall through */ > case I915_FORMAT_MOD_Yf_TILED: > /* > * Bspec seems to suggest that the Yf tile width would >@@ -2156,7 +2164,7 @@ static unsigned int intel_surf_alignment(const struct >drm_framebuffer *fb, > struct drm_i915_private *dev_priv = to_i915(fb->dev); > > /* AUX_DIST needs only 4K alignment */ >- if (fb->format->format == DRM_FORMAT_NV12 && plane == 1) >+ if (plane == 1)
This looks wrong at least within this context, surely multi-planar formats might have different alignment restrictions? > return 4096; > > switch (fb->modifier) { >@@ -2166,6 +2174,8 @@ static unsigned int intel_surf_alignment(const struct >drm_framebuffer *fb, > if (INTEL_GEN(dev_priv) >= 9) > return 256 * 1024; > return 0; >+ case I915_FORMAT_MOD_Y_TILED_CCS: >+ case I915_FORMAT_MOD_Yf_TILED_CCS: > case I915_FORMAT_MOD_Y_TILED: > case I915_FORMAT_MOD_Yf_TILED: > return 1 * 1024 * 1024; >@@ -2472,6 +2482,7 @@ static unsigned int intel_fb_modifier_to_tiling(uint64_t >fb_modifier) > case I915_FORMAT_MOD_X_TILED: > return I915_TILING_X; > case I915_FORMAT_MOD_Y_TILED: >+ case I915_FORMAT_MOD_Y_TILED_CCS: Is I915_FORMAT_MOD_Yf_TILED_CCS supposed to be here? > return I915_TILING_Y; > default: > return I915_TILING_NONE; >@@ -2536,6 +2547,35 @@ intel_fill_fb_info(struct drm_i915_private *dev_priv, > > intel_fb_offset_to_xy(&x, &y, fb, i); > >+ if ((fb->modifier == I915_FORMAT_MOD_Y_TILED_CCS || >+ fb->modifier == I915_FORMAT_MOD_Yf_TILED_CCS) && i == 1) { In one of my branches I turned this into a macro/inline because I ended up checking it pretty often (if modifier is CCS type and plane == 1). Just a thought. >+ int main_x, main_y; >+ int ccs_x, ccs_y; >+ >+ /* >+ * Each byte of CCS corresponds to a 16x8 area of the >main surface, and >+ * each CCS tile is 64x64 bytes. >+ */ >+ ccs_x = (x * 16) % (64 * 16); >+ ccs_y = (y * 8) % (64 * 8); >+ main_x = intel_fb->normal[0].x % (64 * 16); >+ main_y = intel_fb->normal[0].y % (64 * 8); >+ >+ /* >+ * CCS doesn't have its own x/y offset register, so the >intra CCS tile >+ * x/y offsets must match between CCS and the main >surface. >+ */ >+ if (main_x != ccs_x || main_y != ccs_y) { >+ DRM_DEBUG_KMS("Bad CCS x/y (main %d,%d ccs >%d,%d) full (main %d,%d ccs %d,%d)\n", >+ main_x, main_y, >+ ccs_x, ccs_y, >+ intel_fb->normal[0].x, >+ intel_fb->normal[0].y, >+ x, y); >+ return -EINVAL; >+ } >+ } >+ > /* > * The fence (if used) is aligned to the start of the object > * so having the framebuffer wrap around across the edge of the >@@ -2873,6 +2913,9 @@ static int skl_max_plane_width(const struct >drm_framebuffer *fb, int plane, > break; > } > break; >+ case I915_FORMAT_MOD_Y_TILED_CCS: >+ case I915_FORMAT_MOD_Yf_TILED_CCS: >+ /* FIXME AUX plane? */ > case I915_FORMAT_MOD_Y_TILED: > case I915_FORMAT_MOD_Yf_TILED: > switch (cpp) { >@@ -2895,6 +2938,42 @@ static int skl_max_plane_width(const struct >drm_framebuffer *fb, int plane, > return 2048; > } > >+static bool skl_check_main_ccs_coordinates(struct intel_plane_state >*plane_state, >+ int main_x, int main_y, u32 >main_offset) >+{ >+ const struct drm_framebuffer *fb = plane_state->base.fb; >+ int aux_x = plane_state->aux.x; >+ int aux_y = plane_state->aux.y; >+ u32 aux_offset = plane_state->aux.offset; >+ u32 alignment = intel_surf_alignment(fb, 1); >+ >+ while (aux_offset >= main_offset && aux_y <= main_y) { >+ int x, y; >+ >+ if (aux_x == main_x && aux_y == main_y) >+ break; >+ >+ if (aux_offset == 0) >+ break; >+ >+ x = aux_x / 16; >+ y = aux_y / 8; >+ aux_offset = intel_adjust_tile_offset(&x, &y, plane_state, 1, >+ aux_offset, aux_offset - >alignment); >+ aux_x = x * 16 + aux_x % 16; >+ aux_y = y * 8 + aux_y % 8; >+ } >+ >+ if (aux_x != main_x || aux_y != main_y) >+ return false; >+ >+ plane_state->aux.offset = aux_offset; >+ plane_state->aux.x = aux_x; >+ plane_state->aux.y = aux_y; >+ >+ return true; >+} >+ > static int skl_check_main_surface(struct intel_plane_state *plane_state) > { > const struct drm_framebuffer *fb = plane_state->base.fb; >@@ -2937,7 +3016,7 @@ static int skl_check_main_surface(struct >intel_plane_state *plane_state) > > while ((x + w) * cpp > fb->pitches[0]) { > if (offset == 0) { >- DRM_DEBUG_KMS("Unable to find suitable display >surface offset\n"); >+ DRM_DEBUG_KMS("Unable to find suitable display >surface offset due to X-tiling\n"); > return -EINVAL; > } > >@@ -2946,6 +3025,26 @@ static int skl_check_main_surface(struct >intel_plane_state *plane_state) > } > } > >+ /* >+ * CCS AUX surface doesn't have its own x/y offsets, we must make sure >+ * they match with the main surface x/y offsets. >+ */ >+ if (fb->modifier == I915_FORMAT_MOD_Y_TILED_CCS || >+ fb->modifier == I915_FORMAT_MOD_Yf_TILED_CCS) { >+ while (!skl_check_main_ccs_coordinates(plane_state, x, y, >offset)) { >+ if (offset == 0) >+ break; >+ >+ offset = intel_adjust_tile_offset(&x, &y, plane_state, >0, >+ offset, offset - >alignment); >+ } >+ >+ if (x != plane_state->aux.x || y != plane_state->aux.y) { >+ DRM_DEBUG_KMS("Unable to find suitable display surface >offset due to CCS\n"); >+ return -EINVAL; >+ } >+ } >+ > plane_state->main.offset = offset; > plane_state->main.x = x; > plane_state->main.y = y; >@@ -2982,6 +3081,53 @@ static int skl_check_nv12_aux_surface(struct >intel_plane_state *plane_state) > return 0; > } > >+static int skl_check_ccs_aux_surface(struct intel_plane_state *plane_state) >+{ >+ struct intel_plane *plane = to_intel_plane(plane_state->base.plane); >+ struct intel_crtc *crtc = to_intel_crtc(plane_state->base.crtc); >+ int src_x = plane_state->base.src.x1 >> 16; >+ int src_y = plane_state->base.src.y1 >> 16; >+ int x = src_x / 16; >+ int y = src_y / 8; >+ u32 offset; >+ >+ switch (plane->id) { >+ case PLANE_PRIMARY: >+ case PLANE_SPRITE0: >+ break; >+ default: >+ DRM_DEBUG_KMS("RC support only on plane 1 and 2\n"); >+ return -EINVAL; >+ } >+ >+ if (crtc->pipe == PIPE_C) { >+ DRM_DEBUG_KMS("No RC support on pipe C\n"); >+ return -EINVAL; >+ } >+ /* >+ * TODO: >+ * 1. Disable stereo 3D when render decomp is enabled (bit 7:6) >+ * 2. Render decompression must not be used in VTd pass-through mode >+ * 3. Program hashing select CHICKEN_MISC1 bit 15 >+ */ >+ >+ if (plane_state->base.rotation && >+ plane_state->base.rotation & ~(DRM_ROTATE_0 | DRM_ROTATE_180)) { >+ DRM_DEBUG_KMS("RC support only with 0/180 degree rotation %x\n", >+ plane_state->base.rotation); >+ return -EINVAL; >+ } >+ >+ intel_add_fb_offsets(&x, &y, plane_state, 1); >+ offset = intel_compute_tile_offset(&x, &y, plane_state, 1); >+ >+ plane_state->aux.offset = offset; >+ plane_state->aux.x = x * 16 + src_x % 16; >+ plane_state->aux.y = y * 8 + src_y % 8; >+ >+ return 0; >+} >+ > int skl_check_plane_surface(struct intel_plane_state *plane_state) > { > const struct drm_framebuffer *fb = plane_state->base.fb; >@@ -3002,6 +3148,11 @@ int skl_check_plane_surface(struct intel_plane_state >*plane_state) > ret = skl_check_nv12_aux_surface(plane_state); > if (ret) > return ret; >+ } else if (fb->modifier == I915_FORMAT_MOD_Y_TILED_CCS || >+ fb->modifier == I915_FORMAT_MOD_Yf_TILED_CCS) { >+ ret = skl_check_ccs_aux_surface(plane_state); >+ if (ret) >+ return ret; > } else { > plane_state->aux.offset = ~0xfff; > plane_state->aux.x = 0; >@@ -3357,8 +3508,12 @@ u32 skl_plane_ctl_tiling(uint64_t fb_modifier) > return PLANE_CTL_TILED_X; > case I915_FORMAT_MOD_Y_TILED: > return PLANE_CTL_TILED_Y; >+ case I915_FORMAT_MOD_Y_TILED_CCS: >+ return PLANE_CTL_TILED_Y | PLANE_CTL_DECOMPRESSION_ENABLE; > case I915_FORMAT_MOD_Yf_TILED: > return PLANE_CTL_TILED_YF; >+ case I915_FORMAT_MOD_Yf_TILED_CCS: >+ return PLANE_CTL_TILED_YF | PLANE_CTL_DECOMPRESSION_ENABLE; > default: > MISSING_CASE(fb_modifier); > } >@@ -3401,6 +3556,7 @@ static void skylake_update_primary_plane(struct >drm_plane *plane, > u32 plane_ctl; > unsigned int rotation = plane_state->base.rotation; > u32 stride = skl_plane_stride(fb, 0, rotation); >+ u32 aux_stride = skl_plane_stride(fb, 1, rotation); > u32 surf_addr = plane_state->main.offset; > int scaler_id = plane_state->scaler_id; > int src_x = plane_state->main.x; >@@ -3436,6 +3592,10 @@ static void skylake_update_primary_plane(struct >drm_plane *plane, > I915_WRITE(PLANE_OFFSET(pipe, plane_id), (src_y << 16) | src_x); > I915_WRITE(PLANE_STRIDE(pipe, plane_id), stride); > I915_WRITE(PLANE_SIZE(pipe, plane_id), (src_h << 16) | src_w); >+ I915_WRITE(PLANE_AUX_DIST(pipe, plane_id), >+ (plane_state->aux.offset - surf_addr) | aux_stride); >+ I915_WRITE(PLANE_AUX_OFFSET(pipe, plane_id), >+ (plane_state->aux.y << 16) | plane_state->aux.x); > > if (scaler_id >= 0) { > uint32_t ps_ctrl = 0; >@@ -9807,10 +9967,16 @@ skylake_get_initial_plane_config(struct intel_crtc >*crtc, > fb->modifier = I915_FORMAT_MOD_X_TILED; > break; > case PLANE_CTL_TILED_Y: >- fb->modifier = I915_FORMAT_MOD_Y_TILED; >+ if (val & PLANE_CTL_DECOMPRESSION_ENABLE) >+ fb->modifier = I915_FORMAT_MOD_Y_TILED_CCS; >+ else >+ fb->modifier = I915_FORMAT_MOD_Y_TILED; > break; > case PLANE_CTL_TILED_YF: >- fb->modifier = I915_FORMAT_MOD_Yf_TILED; >+ if (val & PLANE_CTL_DECOMPRESSION_ENABLE) >+ fb->modifier = I915_FORMAT_MOD_Yf_TILED_CCS; >+ else >+ fb->modifier = I915_FORMAT_MOD_Yf_TILED; I'm wondering if this is actually feasible. If we find compression enabled, I'd think we should just disable it. > break; > default: > MISSING_CASE(tiling); >@@ -12014,7 +12180,7 @@ static void skl_do_mmio_flip(struct intel_crtc >*intel_crtc, > u32 ctl, stride = skl_plane_stride(fb, 0, rotation); > > ctl = I915_READ(PLANE_CTL(pipe, 0)); >- ctl &= ~PLANE_CTL_TILED_MASK; >+ ctl &= ~(PLANE_CTL_TILED_MASK | PLANE_CTL_DECOMPRESSION_ENABLE); > switch (fb->modifier) { > case DRM_FORMAT_MOD_NONE: > break; >@@ -12024,9 +12190,15 @@ static void skl_do_mmio_flip(struct intel_crtc >*intel_crtc, > case I915_FORMAT_MOD_Y_TILED: > ctl |= PLANE_CTL_TILED_Y; > break; >+ case I915_FORMAT_MOD_Y_TILED_CCS: >+ ctl |= PLANE_CTL_TILED_Y | PLANE_CTL_DECOMPRESSION_ENABLE; >+ break; > case I915_FORMAT_MOD_Yf_TILED: > ctl |= PLANE_CTL_TILED_YF; > break; >+ case I915_FORMAT_MOD_Yf_TILED_CCS: >+ ctl |= PLANE_CTL_TILED_YF | PLANE_CTL_DECOMPRESSION_ENABLE; >+ break; > default: > MISSING_CASE(fb->modifier); > } >@@ -15926,8 +16098,8 @@ static int intel_framebuffer_init(struct drm_device >*dev, > { > struct drm_i915_private *dev_priv = to_i915(dev); > unsigned int tiling = i915_gem_object_get_tiling(obj); >- int ret; >- u32 pitch_limit, stride_alignment; >+ int ret, i; >+ u32 pitch_limit; > struct drm_format_name_buf format_name; > > WARN_ON(!mutex_is_locked(&dev->struct_mutex)); >@@ -15953,6 +16125,19 @@ static int intel_framebuffer_init(struct drm_device >*dev, > > /* Passed in modifier sanity checking. */ > switch (mode_cmd->modifier[0]) { >+ case I915_FORMAT_MOD_Y_TILED_CCS: >+ case I915_FORMAT_MOD_Yf_TILED_CCS: >+ switch (mode_cmd->pixel_format) { >+ case DRM_FORMAT_XBGR8888: >+ case DRM_FORMAT_ABGR8888: >+ case DRM_FORMAT_XRGB8888: >+ case DRM_FORMAT_ARGB8888: >+ break; >+ default: >+ DRM_DEBUG_KMS("RC supported only with RGB8888 >formats\n"); >+ return -EINVAL; >+ } >+ /* fall through */ > case I915_FORMAT_MOD_Y_TILED: > case I915_FORMAT_MOD_Yf_TILED: > if (INTEL_GEN(dev_priv) < 9) { >@@ -16061,11 +16246,21 @@ static int intel_framebuffer_init(struct drm_device >*dev, > > drm_helper_mode_fill_fb_struct(dev, &intel_fb->base, mode_cmd); > >- stride_alignment = intel_fb_stride_alignment(&intel_fb->base, 0); >- if (mode_cmd->pitches[0] & (stride_alignment - 1)) { >- DRM_DEBUG_KMS("pitch (%d) must be at least %u byte aligned\n", >- mode_cmd->pitches[0], stride_alignment); >- return -EINVAL; >+ for (i = 0; i < intel_fb->base.format->num_planes; i++) { >+ u32 stride_alignment; >+ >+ if (mode_cmd->handles[i] != mode_cmd->handles[0]) { >+ DRM_DEBUG_KMS("bad plane %d handle\n", i); >+ return -EINVAL; >+ } >+ >+ stride_alignment = intel_fb_stride_alignment(&intel_fb->base, >i); >+ >+ if (mode_cmd->pitches[i] & (stride_alignment - 1)) { >+ DRM_DEBUG_KMS("plane %d pitch (%d) must be at least %u >byte aligned\n", >+ i, mode_cmd->pitches[i], >stride_alignment); >+ return -EINVAL; >+ } > } > > intel_fb->obj = obj; >diff --git a/drivers/gpu/drm/i915/intel_pm.c b/drivers/gpu/drm/i915/intel_pm.c >index 249623d45be0..add359022c96 100644 >--- a/drivers/gpu/drm/i915/intel_pm.c >+++ b/drivers/gpu/drm/i915/intel_pm.c >@@ -3314,7 +3314,9 @@ skl_ddb_min_alloc(const struct drm_plane_state *pstate, > > /* For Non Y-tile return 8-blocks */ > if (fb->modifier != I915_FORMAT_MOD_Y_TILED && >- fb->modifier != I915_FORMAT_MOD_Yf_TILED) >+ fb->modifier != I915_FORMAT_MOD_Yf_TILED && >+ fb->modifier != I915_FORMAT_MOD_Y_TILED_CCS && >+ fb->modifier != I915_FORMAT_MOD_Yf_TILED_CCS) > return 8; > > src_w = drm_rect_width(&intel_pstate->base.src) >> 16; >@@ -3590,7 +3592,9 @@ static int skl_compute_plane_wm(const struct >drm_i915_private *dev_priv, > } > > y_tiled = fb->modifier == I915_FORMAT_MOD_Y_TILED || >- fb->modifier == I915_FORMAT_MOD_Yf_TILED; >+ fb->modifier == I915_FORMAT_MOD_Yf_TILED || >+ fb->modifier == I915_FORMAT_MOD_Y_TILED_CCS || >+ fb->modifier == I915_FORMAT_MOD_Yf_TILED_CCS; > x_tiled = fb->modifier == I915_FORMAT_MOD_X_TILED; > > /* Display WA #1141: kbl. */ >diff --git a/drivers/gpu/drm/i915/intel_sprite.c >b/drivers/gpu/drm/i915/intel_sprite.c >index 7031bc733d97..063a994815d0 100644 >--- a/drivers/gpu/drm/i915/intel_sprite.c >+++ b/drivers/gpu/drm/i915/intel_sprite.c >@@ -210,6 +210,7 @@ skl_update_plane(struct drm_plane *drm_plane, > u32 surf_addr = plane_state->main.offset; > unsigned int rotation = plane_state->base.rotation; > u32 stride = skl_plane_stride(fb, 0, rotation); >+ u32 aux_stride = skl_plane_stride(fb, 1, rotation); > int crtc_x = plane_state->base.dst.x1; > int crtc_y = plane_state->base.dst.y1; > uint32_t crtc_w = drm_rect_width(&plane_state->base.dst); >@@ -248,6 +249,10 @@ skl_update_plane(struct drm_plane *drm_plane, > I915_WRITE(PLANE_OFFSET(pipe, plane_id), (y << 16) | x); > I915_WRITE(PLANE_STRIDE(pipe, plane_id), stride); > I915_WRITE(PLANE_SIZE(pipe, plane_id), (src_h << 16) | src_w); >+ I915_WRITE(PLANE_AUX_DIST(pipe, plane_id), >+ (plane_state->aux.offset - surf_addr) | aux_stride); >+ I915_WRITE(PLANE_AUX_OFFSET(pipe, plane_id), >+ (plane_state->aux.y << 16) | plane_state->aux.x); > > /* program plane scaler */ > if (plane_state->scaler_id >= 0) { Looks good. I haven't tested it. Reviewed-by: Ben Widawsky <ben at bwidawsk.net>