diff options
Diffstat (limited to 'drivers/gpu/drm/amd/display/dc/dce110')
9 files changed, 194 insertions, 102 deletions
diff --git a/drivers/gpu/drm/amd/display/dc/dce110/dce110_compressor.c b/drivers/gpu/drm/amd/display/dc/dce110/dce110_compressor.c index 7b23239d33fe..72b580a4eb85 100644 --- a/drivers/gpu/drm/amd/display/dc/dce110/dce110_compressor.c +++ b/drivers/gpu/drm/amd/display/dc/dce110/dce110_compressor.c @@ -23,6 +23,9 @@ * */ +#include <linux/delay.h> +#include <linux/slab.h> + #include "dm_services.h" #include "dce/dce_11_0_d.h" diff --git a/drivers/gpu/drm/amd/display/dc/dce110/dce110_hw_sequencer.c b/drivers/gpu/drm/amd/display/dc/dce110/dce110_hw_sequencer.c index 7ac50ab1b762..858a58856ebd 100644 --- a/drivers/gpu/drm/amd/display/dc/dce110/dce110_hw_sequencer.c +++ b/drivers/gpu/drm/amd/display/dc/dce110/dce110_hw_sequencer.c @@ -22,6 +22,9 @@ * Authors: AMD * */ + +#include <linux/delay.h> + #include "dm_services.h" #include "dc.h" #include "dc_bios_types.h" @@ -46,6 +49,7 @@ #include "link_encoder.h" #include "link_hwss.h" #include "clock_source.h" +#include "clk_mgr.h" #include "abm.h" #include "audio.h" #include "reg_helper.h" @@ -242,6 +246,9 @@ static void build_prescale_params(struct ipp_prescale_params *prescale_params, prescale_params->mode = IPP_PRESCALE_MODE_FIXED_UNSIGNED; switch (plane_state->format) { + case SURFACE_PIXEL_FORMAT_GRPH_RGB565: + prescale_params->scale = 0x2082; + break; case SURFACE_PIXEL_FORMAT_GRPH_ARGB8888: case SURFACE_PIXEL_FORMAT_GRPH_ABGR8888: prescale_params->scale = 0x2020; @@ -662,6 +669,26 @@ void dce110_enable_stream(struct pipe_ctx *pipe_ctx) /* update AVI info frame (HDMI, DP)*/ /* TODO: FPGA may change to hwss.update_info_frame */ + +#if defined(CONFIG_DRM_AMD_DC_DCN2_0) + if (pipe_ctx->stream_res.stream_enc->funcs->set_dynamic_metadata != NULL && + pipe_ctx->plane_res.hubp != NULL) { + if (pipe_ctx->stream->dmdata_address.quad_part != 0) { + /* if using dynamic meta, don't set up generic infopackets */ + pipe_ctx->stream_res.encoder_info_frame.hdrsmd.valid = false; + pipe_ctx->stream_res.stream_enc->funcs->set_dynamic_metadata( + pipe_ctx->stream_res.stream_enc, + true, pipe_ctx->plane_res.hubp->inst, + dc_is_dp_signal(pipe_ctx->stream->signal) ? + dmdata_dp : dmdata_hdmi); + } else + pipe_ctx->stream_res.stream_enc->funcs->set_dynamic_metadata( + pipe_ctx->stream_res.stream_enc, + false, pipe_ctx->plane_res.hubp->inst, + dc_is_dp_signal(pipe_ctx->stream->signal) ? + dmdata_dp : dmdata_hdmi); + } +#endif dce110_update_info_frame(pipe_ctx); /* enable early control to avoid corruption on DP monitor*/ @@ -935,28 +962,17 @@ void hwss_edp_backlight_control( edp_receiver_ready_T9(link); } -// Static helper function which calls the correct function -// based on pp_smu version -static void set_pme_wa_enable_by_version(struct dc *dc) -{ - struct pp_smu_funcs *pp_smu = NULL; - - if (dc->res_pool->pp_smu) - pp_smu = dc->res_pool->pp_smu; - - if (pp_smu) { - if (pp_smu->ctx.ver == PP_SMU_VER_RV && pp_smu->rv_funcs.set_pme_wa_enable) - pp_smu->rv_funcs.set_pme_wa_enable(&(pp_smu->ctx)); - } -} - void dce110_enable_audio_stream(struct pipe_ctx *pipe_ctx) { /* notify audio driver for audio modes of monitor */ struct dc *core_dc = pipe_ctx->stream->ctx->dc; struct pp_smu_funcs *pp_smu = NULL; + struct clk_mgr *clk_mgr = core_dc->clk_mgr; unsigned int i, num_audio = 1; + if (pipe_ctx->stream_res.audio && pipe_ctx->stream_res.audio->enabled == true) + return; + if (core_dc->res_pool->pp_smu) pp_smu = core_dc->res_pool->pp_smu; @@ -969,13 +985,15 @@ void dce110_enable_audio_stream(struct pipe_ctx *pipe_ctx) pipe_ctx->stream_res.audio->funcs->az_enable(pipe_ctx->stream_res.audio); - if (num_audio >= 1 && pp_smu != NULL) + if (num_audio >= 1 && clk_mgr->funcs->enable_pme_wa) /*this is the first audio. apply the PME w/a in order to wake AZ from D3*/ - set_pme_wa_enable_by_version(core_dc); + clk_mgr->funcs->enable_pme_wa(clk_mgr); /* un-mute audio */ /* TODO: audio should be per stream rather than per link */ pipe_ctx->stream_res.stream_enc->funcs->audio_mute_control( pipe_ctx->stream_res.stream_enc, false); + if (pipe_ctx->stream_res.audio) + pipe_ctx->stream_res.audio->enabled = true; } } @@ -983,6 +1001,10 @@ void dce110_disable_audio_stream(struct pipe_ctx *pipe_ctx, int option) { struct dc *dc = pipe_ctx->stream->ctx->dc; struct pp_smu_funcs *pp_smu = NULL; + struct clk_mgr *clk_mgr = dc->clk_mgr; + + if (pipe_ctx->stream_res.audio && pipe_ctx->stream_res.audio->enabled == false) + return; pipe_ctx->stream_res.stream_enc->funcs->audio_mute_control( pipe_ctx->stream_res.stream_enc, true); @@ -1008,15 +1030,17 @@ void dce110_disable_audio_stream(struct pipe_ctx *pipe_ctx, int option) update_audio_usage(&dc->current_state->res_ctx, dc->res_pool, pipe_ctx->stream_res.audio, false); pipe_ctx->stream_res.audio = NULL; } - if (pp_smu != NULL) + if (clk_mgr->funcs->enable_pme_wa) /*this is the first audio. apply the PME w/a in order to wake AZ from D3*/ - set_pme_wa_enable_by_version(dc); + clk_mgr->funcs->enable_pme_wa(clk_mgr); /* TODO: notify audio driver for if audio modes list changed * add audio mode list change flag */ /* dal_audio_disable_azalia_audio_jack_presence(stream->audio, * stream->stream_engine_id); */ + if (pipe_ctx->stream_res.audio) + pipe_ctx->stream_res.audio->enabled = false; } } @@ -1296,6 +1320,11 @@ static enum dc_status dce110_enable_stream_timing( pipe_ctx->stream_res.tg->funcs->program_timing( pipe_ctx->stream_res.tg, &stream->timing, + 0, + 0, + 0, + 0, + pipe_ctx->stream->signal, true); } @@ -1318,6 +1347,9 @@ static enum dc_status apply_single_controller_ctx_to_hw( struct dc_stream_state *stream = pipe_ctx->stream; struct drr_params params = {0}; unsigned int event_triggers = 0; +#if defined(CONFIG_DRM_AMD_DC_DCN2_0) + struct pipe_ctx *odm_pipe = dc_res_get_odm_bottom_pipe(pipe_ctx); +#endif if (dc->hwss.disable_stream_gating) { dc->hwss.disable_stream_gating(dc, pipe_ctx); @@ -1383,6 +1415,20 @@ static enum dc_status apply_single_controller_ctx_to_hw( pipe_ctx->stream_res.opp, &stream->bit_depth_params, &stream->clamping); +#if defined(CONFIG_DRM_AMD_DC_DCN2_0) + if (odm_pipe) { + odm_pipe->stream_res.opp->funcs->opp_set_dyn_expansion( + odm_pipe->stream_res.opp, + COLOR_SPACE_YCBCR601, + stream->timing.display_color_depth, + stream->signal); + + odm_pipe->stream_res.opp->funcs->opp_program_fmt( + odm_pipe->stream_res.opp, + &stream->bit_depth_params, + &stream->clamping); + } +#endif if (!stream->dpms_off) core_link_enable_stream(context, pipe_ctx); @@ -1488,10 +1534,23 @@ static void disable_vga_and_power_gate_all_controllers( } } -static struct dc_link *get_link_for_edp(struct dc *dc) + +static struct dc_stream_state *get_edp_stream(struct dc_state *context) { int i; + for (i = 0; i < context->stream_count; i++) { + if (context->streams[i]->signal == SIGNAL_TYPE_EDP) + return context->streams[i]; + } + return NULL; +} + +static struct dc_link *get_edp_link(struct dc *dc) +{ + int i; + + // report any eDP links, even unconnected DDI's for (i = 0; i < dc->link_count; i++) { if (dc->links[i]->connector_signal == SIGNAL_TYPE_EDP) return dc->links[i]; @@ -1499,23 +1558,13 @@ static struct dc_link *get_link_for_edp(struct dc *dc) return NULL; } -static struct dc_link *get_link_for_edp_to_turn_off( +static struct dc_link *get_edp_link_with_sink( struct dc *dc, struct dc_state *context) { int i; struct dc_link *link = NULL; - /* check if eDP panel is suppose to be set mode, if yes, no need to disable */ - for (i = 0; i < context->stream_count; i++) { - if (context->streams[i]->signal == SIGNAL_TYPE_EDP) { - if (context->streams[i]->dpms_off == true) - return context->streams[i]->sink->link; - else - return NULL; - } - } - /* check if there is an eDP panel not in use */ for (i = 0; i < dc->link_count; i++) { if (dc->links[i]->local_sink && @@ -1538,59 +1587,58 @@ static struct dc_link *get_link_for_edp_to_turn_off( void dce110_enable_accelerated_mode(struct dc *dc, struct dc_state *context) { int i; - struct dc_link *edp_link_to_turnoff = NULL; - struct dc_link *edp_link = get_link_for_edp(dc); - bool can_edp_fast_boot_optimize = false; - bool apply_edp_fast_boot_optimization = false; + struct dc_link *edp_link_with_sink = get_edp_link_with_sink(dc, context); + struct dc_link *edp_link = get_edp_link(dc); + struct dc_stream_state *edp_stream = NULL; + bool can_apply_edp_fast_boot = false; bool can_apply_seamless_boot = false; - - for (i = 0; i < context->stream_count; i++) { - if (context->streams[i]->apply_seamless_boot_optimization) { - can_apply_seamless_boot = true; - break; - } - } + bool keep_edp_vdd_on = false; if (dc->hwss.init_pipes) dc->hwss.init_pipes(dc, context); - if (edp_link) { - /* this seems to cause blank screens on DCE8 */ - if ((dc->ctx->dce_version == DCE_VERSION_8_0) || - (dc->ctx->dce_version == DCE_VERSION_8_1) || - (dc->ctx->dce_version == DCE_VERSION_8_3)) - can_edp_fast_boot_optimize = false; - else - can_edp_fast_boot_optimize = - edp_link->link_enc->funcs->is_dig_enabled(edp_link->link_enc); - } + edp_stream = get_edp_stream(context); - if (can_edp_fast_boot_optimize) - edp_link_to_turnoff = get_link_for_edp_to_turn_off(dc, context); + // Check fastboot support, disable on DCE8 because of blank screens + if (edp_link && dc->ctx->dce_version != DCE_VERSION_8_0 && + dc->ctx->dce_version != DCE_VERSION_8_1 && + dc->ctx->dce_version != DCE_VERSION_8_3) { - /* if OS doesn't light up eDP and eDP link is available, we want to disable - * If resume from S4/S5, should optimization. - */ - if (can_edp_fast_boot_optimize && !edp_link_to_turnoff) { - /* Find eDP stream and set optimization flag */ - for (i = 0; i < context->stream_count; i++) { - if (context->streams[i]->signal == SIGNAL_TYPE_EDP) { - context->streams[i]->apply_edp_fast_boot_optimization = true; - apply_edp_fast_boot_optimization = true; + // enable fastboot if backend is enabled on eDP + if (edp_link->link_enc->funcs->is_dig_enabled(edp_link->link_enc)) { + /* Set optimization flag on eDP stream*/ + if (edp_stream) { + edp_stream->apply_edp_fast_boot_optimization = true; + can_apply_edp_fast_boot = true; } } + + // We are trying to enable eDP, don't power down VDD + if (edp_stream) + keep_edp_vdd_on = true; + } + + // Check seamless boot support + for (i = 0; i < context->stream_count; i++) { + if (context->streams[i]->apply_seamless_boot_optimization) { + can_apply_seamless_boot = true; + break; + } } - if (!apply_edp_fast_boot_optimization && !can_apply_seamless_boot) { - if (edp_link_to_turnoff) { + /* eDP should not have stream in resume from S4 and so even with VBios post + * it should get turned off + */ + if (!can_apply_edp_fast_boot && !can_apply_seamless_boot) { + if (edp_link_with_sink && !keep_edp_vdd_on) { /*turn off backlight before DP_blank and encoder powered down*/ - dc->hwss.edp_backlight_control(edp_link_to_turnoff, false); + dc->hwss.edp_backlight_control(edp_link_with_sink, false); } /*resume from S3, no vbios posting, no need to power down again*/ power_down_all_hw_blocks(dc); disable_vga_and_power_gate_all_controllers(dc); - if (edp_link_to_turnoff) - dc->hwss.edp_power_control(edp_link_to_turnoff, false); + if (edp_link_with_sink && !keep_edp_vdd_on) + dc->hwss.edp_power_control(edp_link_with_sink, false); } bios_set_scratch_acc_mode_change(dc->ctx->dc_bios); } @@ -2030,8 +2078,10 @@ enum dc_status dce110_apply_ctx_to_hw( if (pipe_ctx->stream == NULL) continue; - if (pipe_ctx->stream == pipe_ctx_old->stream) + if (pipe_ctx->stream == pipe_ctx_old->stream && + pipe_ctx->stream->link->link_state_valid) { continue; + } if (pipe_ctx_old->stream && !pipe_need_reprogram(pipe_ctx_old, pipe_ctx)) continue; @@ -2318,6 +2368,7 @@ static void init_hw(struct dc *dc) struct dc_bios *bp; struct transform *xfm; struct abm *abm; + struct dmcu *dmcu; bp = dc->ctx->dc_bios; for (i = 0; i < dc->res_pool->pipe_count; i++) { @@ -2345,9 +2396,6 @@ static void init_hw(struct dc *dc) * default signal on connector). */ struct dc_link *link = dc->links[i]; - if (link->link_enc->connector.id == CONNECTOR_ID_EDP) - dc->hwss.edp_power_control(link, true); - link->link_enc->funcs->hw_init(link->link_enc); } @@ -2373,6 +2421,10 @@ static void init_hw(struct dc *dc) abm->funcs->abm_init(abm); } + dmcu = dc->res_pool->dmcu; + if (dmcu != NULL && abm != NULL) + abm->dmcu_is_running = dmcu->funcs->is_dmcu_initialized(dmcu); + if (dc->fbc_compressor) dc->fbc_compressor->funcs->power_up_fbc(dc->fbc_compressor); @@ -2383,7 +2435,7 @@ void dce110_prepare_bandwidth( struct dc *dc, struct dc_state *context) { - struct clk_mgr *dccg = dc->res_pool->clk_mgr; + struct clk_mgr *dccg = dc->clk_mgr; dce110_set_safe_displaymarks(&context->res_ctx, dc->res_pool); @@ -2397,7 +2449,7 @@ void dce110_optimize_bandwidth( struct dc *dc, struct dc_state *context) { - struct clk_mgr *dccg = dc->res_pool->clk_mgr; + struct clk_mgr *dccg = dc->clk_mgr; dce110_set_displaymarks(dc, context); diff --git a/drivers/gpu/drm/amd/display/dc/dce110/dce110_opp_regamma_v.c b/drivers/gpu/drm/amd/display/dc/dce110/dce110_opp_regamma_v.c index 9b65b77e8823..34c5e3c7c6d2 100644 --- a/drivers/gpu/drm/amd/display/dc/dce110/dce110_opp_regamma_v.c +++ b/drivers/gpu/drm/amd/display/dc/dce110/dce110_opp_regamma_v.c @@ -23,6 +23,8 @@ * */ +#include <linux/delay.h> + #include "dm_services.h" /* include DCE11 register header files */ diff --git a/drivers/gpu/drm/amd/display/dc/dce110/dce110_resource.c b/drivers/gpu/drm/amd/display/dc/dce110/dce110_resource.c index dcd04e9ea76b..764329264c3b 100644 --- a/drivers/gpu/drm/amd/display/dc/dce110/dce110_resource.c +++ b/drivers/gpu/drm/amd/display/dc/dce110/dce110_resource.c @@ -23,6 +23,8 @@ * */ +#include <linux/slab.h> + #include "dm_services.h" #include "link_encoder.h" @@ -30,8 +32,6 @@ #include "resource.h" #include "dce110/dce110_resource.h" - -#include "dce/dce_clk_mgr.h" #include "include/irq_service_interface.h" #include "dce/dce_audio.h" #include "dce110/dce110_timing_generator.h" @@ -149,18 +149,6 @@ static const struct dce110_timing_generator_offsets dce110_tg_offsets[] = { #define SRI(reg_name, block, id)\ .reg_name = mm ## block ## id ## _ ## reg_name -static const struct clk_mgr_registers disp_clk_regs = { - CLK_COMMON_REG_LIST_DCE_BASE() -}; - -static const struct clk_mgr_shift disp_clk_shift = { - CLK_COMMON_MASK_SH_LIST_DCE_COMMON_BASE(__SHIFT) -}; - -static const struct clk_mgr_mask disp_clk_mask = { - CLK_COMMON_MASK_SH_LIST_DCE_COMMON_BASE(_MASK) -}; - static const struct dce_dmcu_registers dmcu_regs = { DMCU_DCE110_COMMON_REG_LIST() }; @@ -811,9 +799,6 @@ static void destruct(struct dce110_resource_pool *pool) if (pool->base.dmcu != NULL) dce_dmcu_destroy(&pool->base.dmcu); - if (pool->base.clk_mgr != NULL) - dce_clk_mgr_destroy(&pool->base.clk_mgr); - if (pool->base.irqs != NULL) { dal_irq_service_destroy(&pool->base.irqs); } @@ -1097,6 +1082,11 @@ static struct pipe_ctx *dce110_acquire_underlay( pipe_ctx->stream_res.tg->funcs->program_timing(pipe_ctx->stream_res.tg, &stream->timing, + 0, + 0, + 0, + 0, + pipe_ctx->stream->signal, false); pipe_ctx->stream_res.tg->funcs->enable_advanced_request( @@ -1129,6 +1119,38 @@ static void dce110_destroy_resource_pool(struct resource_pool **pool) *pool = NULL; } +struct stream_encoder *dce110_find_first_free_match_stream_enc_for_link( + struct resource_context *res_ctx, + const struct resource_pool *pool, + struct dc_stream_state *stream) +{ + int i; + int j = -1; + struct dc_link *link = stream->link; + + for (i = 0; i < pool->stream_enc_count; i++) { + if (!res_ctx->is_stream_enc_acquired[i] && + pool->stream_enc[i]) { + /* Store first available for MST second display + * in daisy chain use case + */ + j = i; + if (pool->stream_enc[i]->id == + link->link_enc->preferred_engine) + return pool->stream_enc[i]; + } + } + + /* + * For CZ and later, we can allow DIG FE and BE to differ for all display types + */ + + if (j >= 0) + return pool->stream_enc[j]; + + return NULL; +} + static const struct resource_funcs dce110_res_pool_funcs = { .destroy = dce110_destroy_resource_pool, @@ -1137,7 +1159,8 @@ static const struct resource_funcs dce110_res_pool_funcs = { .validate_plane = dce110_validate_plane, .acquire_idle_pipe_for_layer = dce110_acquire_underlay, .add_stream_to_ctx = dce110_add_stream_to_ctx, - .validate_global = dce110_validate_global + .validate_global = dce110_validate_global, + .find_first_free_match_stream_enc_for_link = dce110_find_first_free_match_stream_enc_for_link }; static bool underlay_create(struct dc_context *ctx, struct resource_pool *pool) @@ -1308,16 +1331,6 @@ static bool construct( } } - pool->base.clk_mgr = dce110_clk_mgr_create(ctx, - &disp_clk_regs, - &disp_clk_shift, - &disp_clk_mask); - if (pool->base.clk_mgr == NULL) { - dm_error("DC: failed to create display clock!\n"); - BREAK_TO_DEBUGGER(); - goto res_create_fail; - } - pool->base.dmcu = dce_dmcu_create(ctx, &dmcu_regs, &dmcu_shift, diff --git a/drivers/gpu/drm/amd/display/dc/dce110/dce110_resource.h b/drivers/gpu/drm/amd/display/dc/dce110/dce110_resource.h index e5f168c1f8c8..aa4531e0800e 100644 --- a/drivers/gpu/drm/amd/display/dc/dce110/dce110_resource.h +++ b/drivers/gpu/drm/amd/display/dc/dce110/dce110_resource.h @@ -45,5 +45,10 @@ struct resource_pool *dce110_create_resource_pool( struct dc *dc, struct hw_asic_id asic_id); +struct stream_encoder *dce110_find_first_free_match_stream_enc_for_link( + struct resource_context *res_ctx, + const struct resource_pool *pool, + struct dc_stream_state *stream); + #endif /* __DC_RESOURCE_DCE110_H__ */ diff --git a/drivers/gpu/drm/amd/display/dc/dce110/dce110_timing_generator.c b/drivers/gpu/drm/amd/display/dc/dce110/dce110_timing_generator.c index 1b2fe0df347f..5f7c2c5641c4 100644 --- a/drivers/gpu/drm/amd/display/dc/dce110/dce110_timing_generator.c +++ b/drivers/gpu/drm/amd/display/dc/dce110/dce110_timing_generator.c @@ -1952,6 +1952,11 @@ void dce110_tg_set_overscan_color(struct timing_generator *tg, void dce110_tg_program_timing(struct timing_generator *tg, const struct dc_crtc_timing *timing, + int vready_offset, + int vstartup_start, + int vupdate_offset, + int vupdate_width, + const enum signal_type signal, bool use_vbios) { if (use_vbios) diff --git a/drivers/gpu/drm/amd/display/dc/dce110/dce110_timing_generator.h b/drivers/gpu/drm/amd/display/dc/dce110/dce110_timing_generator.h index 734d4965dab1..768ccf27ada9 100644 --- a/drivers/gpu/drm/amd/display/dc/dce110/dce110_timing_generator.h +++ b/drivers/gpu/drm/amd/display/dc/dce110/dce110_timing_generator.h @@ -256,6 +256,11 @@ void dce110_tg_set_overscan_color(struct timing_generator *tg, void dce110_tg_program_timing(struct timing_generator *tg, const struct dc_crtc_timing *timing, + int vready_offset, + int vstartup_start, + int vupdate_offset, + int vupdate_width, + const enum signal_type signal, bool use_vbios); bool dce110_tg_is_blanked(struct timing_generator *tg); diff --git a/drivers/gpu/drm/amd/display/dc/dce110/dce110_timing_generator_v.c b/drivers/gpu/drm/amd/display/dc/dce110/dce110_timing_generator_v.c index a3cef60380ed..a13a2f58944e 100644 --- a/drivers/gpu/drm/amd/display/dc/dce110/dce110_timing_generator_v.c +++ b/drivers/gpu/drm/amd/display/dc/dce110/dce110_timing_generator_v.c @@ -435,6 +435,11 @@ static void dce110_timing_generator_v_set_blank(struct timing_generator *tg, static void dce110_timing_generator_v_program_timing(struct timing_generator *tg, const struct dc_crtc_timing *timing, + int vready_offset, + int vstartup_start, + int vupdate_offset, + int vupdate_width, + const enum signal_type signal, bool use_vbios) { if (use_vbios) diff --git a/drivers/gpu/drm/amd/display/dc/dce110/dce110_transform_v.c b/drivers/gpu/drm/amd/display/dc/dce110/dce110_transform_v.c index aa8d6b10d2c3..b1aaab5590cc 100644 --- a/drivers/gpu/drm/amd/display/dc/dce110/dce110_transform_v.c +++ b/drivers/gpu/drm/amd/display/dc/dce110/dce110_transform_v.c @@ -23,6 +23,8 @@ * */ +#include <linux/delay.h> + #include "dce110_transform_v.h" #include "dm_services.h" #include "dc.h" |