From: Michal Wajdeczko Date: Mon, 2 Jun 2025 10:33:24 +0000 (+0200) Subject: drm/xe/vf: Move tile-related VF functions to separate file X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=eb9b34734c41a08166e41836bb6a47bc117c77a1;p=thirdparty%2Fkernel%2Flinux.git drm/xe/vf: Move tile-related VF functions to separate file Some of our VF functions, even if they take a GT pointer, work only on primary GT and really are tile-related and would be better to keep them separate from the rest of true GT-oriented functions. Move them to a file and update to take a tile pointer instead. Signed-off-by: Michal Wajdeczko Cc: Tomasz Lis Reviewed-by: Tomasz Lis Link: https://lore.kernel.org/r/20250602103325.549-3-michal.wajdeczko@intel.com --- diff --git a/drivers/gpu/drm/xe/Makefile b/drivers/gpu/drm/xe/Makefile index c5d6681645ede..01d231777901a 100644 --- a/drivers/gpu/drm/xe/Makefile +++ b/drivers/gpu/drm/xe/Makefile @@ -139,7 +139,8 @@ xe-y += \ xe_guc_relay.o \ xe_memirq.o \ xe_sriov.o \ - xe_sriov_vf.o + xe_sriov_vf.o \ + xe_tile_sriov_vf.o xe-$(CONFIG_PCI_IOV) += \ xe_gt_sriov_pf.o \ diff --git a/drivers/gpu/drm/xe/xe_ggtt.c b/drivers/gpu/drm/xe/xe_ggtt.c index af8e53014b874..b9a0fd5ccaba3 100644 --- a/drivers/gpu/drm/xe/xe_ggtt.c +++ b/drivers/gpu/drm/xe/xe_ggtt.c @@ -22,12 +22,12 @@ #include "xe_device.h" #include "xe_gt.h" #include "xe_gt_printk.h" -#include "xe_gt_sriov_vf.h" #include "xe_gt_tlb_invalidation.h" #include "xe_map.h" #include "xe_mmio.h" #include "xe_pm.h" #include "xe_sriov.h" +#include "xe_tile_sriov_vf.h" #include "xe_wa.h" #include "xe_wopcm.h" @@ -258,7 +258,7 @@ int xe_ggtt_init_early(struct xe_ggtt *ggtt) return err; if (IS_SRIOV_VF(xe)) { - err = xe_gt_sriov_vf_prepare_ggtt(xe_tile_get_gt(ggtt->tile, 0)); + err = xe_tile_sriov_vf_prepare_ggtt(ggtt->tile); if (err) return err; } diff --git a/drivers/gpu/drm/xe/xe_gt_sriov_vf.c b/drivers/gpu/drm/xe/xe_gt_sriov_vf.c index acfb3b1b08324..792523cfa6e67 100644 --- a/drivers/gpu/drm/xe/xe_gt_sriov_vf.c +++ b/drivers/gpu/drm/xe/xe_gt_sriov_vf.c @@ -613,168 +613,6 @@ s64 xe_gt_sriov_vf_ggtt_shift(struct xe_gt *gt) return config->ggtt_shift; } -static int vf_init_ggtt_balloons(struct xe_gt *gt) -{ - struct xe_tile *tile = gt_to_tile(gt); - struct xe_ggtt *ggtt = tile->mem.ggtt; - - xe_gt_assert(gt, IS_SRIOV_VF(gt_to_xe(gt))); - xe_gt_assert(gt, !xe_gt_is_media_type(gt)); - - tile->sriov.vf.ggtt_balloon[0] = xe_ggtt_node_init(ggtt); - if (IS_ERR(tile->sriov.vf.ggtt_balloon[0])) - return PTR_ERR(tile->sriov.vf.ggtt_balloon[0]); - - tile->sriov.vf.ggtt_balloon[1] = xe_ggtt_node_init(ggtt); - if (IS_ERR(tile->sriov.vf.ggtt_balloon[1])) { - xe_ggtt_node_fini(tile->sriov.vf.ggtt_balloon[0]); - return PTR_ERR(tile->sriov.vf.ggtt_balloon[1]); - } - - return 0; -} - -/** - * xe_gt_sriov_vf_balloon_ggtt_locked - Insert balloon nodes to limit used GGTT address range. - * @gt: the &xe_gt struct instance - * Return: 0 on success or a negative error code on failure. - */ -int xe_gt_sriov_vf_balloon_ggtt_locked(struct xe_gt *gt) -{ - struct xe_gt_sriov_vf_selfconfig *config = >->sriov.vf.self_config; - struct xe_tile *tile = gt_to_tile(gt); - struct xe_device *xe = gt_to_xe(gt); - u64 start, end; - int err; - - xe_gt_assert(gt, IS_SRIOV_VF(xe)); - xe_gt_assert(gt, !xe_gt_is_media_type(gt)); - lockdep_assert_held(&tile->mem.ggtt->lock); - - if (!config->ggtt_size) - return -ENODATA; - - /* - * VF can only use part of the GGTT as allocated by the PF: - * - * WOPCM GUC_GGTT_TOP - * |<------------ Total GGTT size ------------------>| - * - * VF GGTT base -->|<- size ->| - * - * +--------------------+----------+-----------------+ - * |////////////////////| block |\\\\\\\\\\\\\\\\\| - * +--------------------+----------+-----------------+ - * - * |<--- balloon[0] --->|<-- VF -->|<-- balloon[1] ->| - */ - - start = xe_wopcm_size(xe); - end = config->ggtt_base; - if (end != start) { - err = xe_ggtt_node_insert_balloon_locked(tile->sriov.vf.ggtt_balloon[0], - start, end); - if (err) - return err; - } - - start = config->ggtt_base + config->ggtt_size; - end = GUC_GGTT_TOP; - if (end != start) { - err = xe_ggtt_node_insert_balloon_locked(tile->sriov.vf.ggtt_balloon[1], - start, end); - if (err) { - xe_ggtt_node_remove_balloon_locked(tile->sriov.vf.ggtt_balloon[0]); - return err; - } - } - - return 0; -} - -static int vf_balloon_ggtt(struct xe_gt *gt) -{ - struct xe_ggtt *ggtt = gt_to_tile(gt)->mem.ggtt; - int err; - - mutex_lock(&ggtt->lock); - err = xe_gt_sriov_vf_balloon_ggtt_locked(gt); - mutex_unlock(&ggtt->lock); - - return err; -} - -/** - * xe_gt_sriov_vf_deballoon_ggtt_locked - Remove balloon nodes. - * @gt: the &xe_gt struct instance - */ -void xe_gt_sriov_vf_deballoon_ggtt_locked(struct xe_gt *gt) -{ - struct xe_tile *tile = gt_to_tile(gt); - - xe_tile_assert(tile, IS_SRIOV_VF(tile_to_xe(tile))); - xe_ggtt_node_remove_balloon_locked(tile->sriov.vf.ggtt_balloon[1]); - xe_ggtt_node_remove_balloon_locked(tile->sriov.vf.ggtt_balloon[0]); -} - -static void vf_deballoon_ggtt(struct xe_gt *gt) -{ - struct xe_tile *tile = gt_to_tile(gt); - - mutex_lock(&tile->mem.ggtt->lock); - xe_gt_sriov_vf_deballoon_ggtt_locked(gt); - mutex_unlock(&tile->mem.ggtt->lock); -} - -static void vf_fini_ggtt_balloons(struct xe_gt *gt) -{ - struct xe_tile *tile = gt_to_tile(gt); - - xe_gt_assert(gt, IS_SRIOV_VF(gt_to_xe(gt))); - xe_gt_assert(gt, !xe_gt_is_media_type(gt)); - - xe_ggtt_node_fini(tile->sriov.vf.ggtt_balloon[1]); - xe_ggtt_node_fini(tile->sriov.vf.ggtt_balloon[0]); -} - -static void cleanup_ggtt(struct drm_device *drm, void *arg) -{ - struct xe_gt *gt = arg; - - vf_deballoon_ggtt(gt); - vf_fini_ggtt_balloons(gt); -} - -/** - * xe_gt_sriov_vf_prepare_ggtt - Prepare a VF's GGTT configuration. - * @gt: the &xe_gt - * - * This function is for VF use only. - * - * Return: 0 on success or a negative error code on failure. - */ -int xe_gt_sriov_vf_prepare_ggtt(struct xe_gt *gt) -{ - struct xe_tile *tile = gt_to_tile(gt); - struct xe_device *xe = tile_to_xe(tile); - int err; - - if (xe_gt_is_media_type(gt)) - return 0; - - err = vf_init_ggtt_balloons(gt); - if (err) - return err; - - err = vf_balloon_ggtt(gt); - if (err) { - vf_fini_ggtt_balloons(gt); - return err; - } - - return drmm_add_action_or_reset(&xe->drm, cleanup_ggtt, gt); -} - static int relay_action_handshake(struct xe_gt *gt, u32 *major, u32 *minor) { u32 request[VF2PF_HANDSHAKE_REQUEST_MSG_LEN] = { @@ -870,89 +708,6 @@ failed: return err; } -/** - * DOC: GGTT nodes shifting during VF post-migration recovery - * - * The first fixup applied to the VF KMD structures as part of post-migration - * recovery is shifting nodes within &xe_ggtt instance. The nodes are moved - * from range previously assigned to this VF, into newly provisioned area. - * The changes include balloons, which are resized accordingly. - * - * The balloon nodes are there to eliminate unavailable ranges from use: one - * reserves the GGTT area below the range for current VF, and another one - * reserves area above. - * - * Below is a GGTT layout of example VF, with a certain address range assigned to - * said VF, and inaccessible areas above and below: - * - * 0 4GiB - * |<--------------------------- Total GGTT size ----------------------------->| - * WOPCM GUC_TOP - * |<-------------- Area mappable by xe_ggtt instance ---------------->| - * - * +---+---------------------------------+----------+----------------------+---+ - * |\\\|/////////////////////////////////| VF mem |//////////////////////|\\\| - * +---+---------------------------------+----------+----------------------+---+ - * - * Hardware enforced access rules before migration: - * - * |<------- inaccessible for VF ------->||<-- inaccessible for VF ->| - * - * GGTT nodes used for tracking allocations: - * - * |<---------- balloon ------------>|<- nodes->|<----- balloon ------>| - * - * After the migration, GGTT area assigned to the VF might have shifted, either - * to lower or to higher address. But we expect the total size and extra areas to - * be identical, as migration can only happen between matching platforms. - * Below is an example of GGTT layout of the VF after migration. Content of the - * GGTT for VF has been moved to a new area, and we receive its address from GuC: - * - * +---+----------------------+----------+---------------------------------+---+ - * |\\\|//////////////////////| VF mem |/////////////////////////////////|\\\| - * +---+----------------------+----------+---------------------------------+---+ - * - * Hardware enforced access rules after migration: - * - * |<- inaccessible for VF -->||<------- inaccessible for VF ------->| - * - * So the VF has a new slice of GGTT assigned, and during migration process, the - * memory content was copied to that new area. But the &xe_ggtt nodes are still - * tracking allocations using the old addresses. The nodes within VF owned area - * have to be shifted, and balloon nodes need to be resized to properly mask out - * areas not owned by the VF. - * - * Fixed &xe_ggtt nodes used for tracking allocations: - * - * |<------ balloon ------>|<- nodes->|<----------- balloon ----------->| - * - * Due to use of GPU profiles, we do not expect the old and new GGTT ares to - * overlap; but our node shifting will fix addresses properly regardless. - */ - -/** - * xe_gt_sriov_vf_fixup_ggtt_nodes - Shift GGTT allocations to match assigned range. - * @gt: the &xe_gt struct instance - * @shift: the shift value - * - * Since Global GTT is not virtualized, each VF has an assigned range - * within the global space. This range might have changed during migration, - * which requires all memory addresses pointing to GGTT to be shifted. - */ -void xe_gt_sriov_vf_fixup_ggtt_nodes(struct xe_gt *gt, s64 shift) -{ - struct xe_tile *tile = gt_to_tile(gt); - struct xe_ggtt *ggtt = tile->mem.ggtt; - - xe_gt_assert(gt, !xe_gt_is_media_type(gt)); - - mutex_lock(&ggtt->lock); - xe_gt_sriov_vf_deballoon_ggtt_locked(gt); - xe_ggtt_shift_nodes_locked(ggtt, shift); - xe_gt_sriov_vf_balloon_ggtt_locked(gt); - mutex_unlock(&ggtt->lock); -} - /** * xe_gt_sriov_vf_migrated_event_handler - Start a VF migration recovery, * or just mark that a GuC is ready for it. diff --git a/drivers/gpu/drm/xe/xe_gt_sriov_vf.h b/drivers/gpu/drm/xe/xe_gt_sriov_vf.h index 2f96ac0c5dcaf..6250fe774d89a 100644 --- a/drivers/gpu/drm/xe/xe_gt_sriov_vf.h +++ b/drivers/gpu/drm/xe/xe_gt_sriov_vf.h @@ -17,10 +17,6 @@ int xe_gt_sriov_vf_bootstrap(struct xe_gt *gt); int xe_gt_sriov_vf_query_config(struct xe_gt *gt); int xe_gt_sriov_vf_connect(struct xe_gt *gt); int xe_gt_sriov_vf_query_runtime(struct xe_gt *gt); -int xe_gt_sriov_vf_prepare_ggtt(struct xe_gt *gt); -int xe_gt_sriov_vf_balloon_ggtt_locked(struct xe_gt *gt); -void xe_gt_sriov_vf_deballoon_ggtt_locked(struct xe_gt *gt); -void xe_gt_sriov_vf_fixup_ggtt_nodes(struct xe_gt *gt, s64 shift); int xe_gt_sriov_vf_notify_resfix_done(struct xe_gt *gt); void xe_gt_sriov_vf_migrated_event_handler(struct xe_gt *gt); diff --git a/drivers/gpu/drm/xe/xe_sriov_vf.c b/drivers/gpu/drm/xe/xe_sriov_vf.c index 46466932375c2..6526fe450e553 100644 --- a/drivers/gpu/drm/xe/xe_sriov_vf.c +++ b/drivers/gpu/drm/xe/xe_sriov_vf.c @@ -15,6 +15,7 @@ #include "xe_sriov.h" #include "xe_sriov_printk.h" #include "xe_sriov_vf.h" +#include "xe_tile_sriov_vf.h" /** * DOC: VF restore procedure in PF KMD and VF KMD @@ -211,7 +212,7 @@ static bool vf_post_migration_fixup_ggtt_nodes(struct xe_device *xe) shift = xe_gt_sriov_vf_ggtt_shift(gt); if (shift) { need_fixups = true; - xe_gt_sriov_vf_fixup_ggtt_nodes(gt, shift); + xe_tile_sriov_vf_fixup_ggtt_nodes(tile, shift); } } return need_fixups; diff --git a/drivers/gpu/drm/xe/xe_tile_sriov_vf.c b/drivers/gpu/drm/xe/xe_tile_sriov_vf.c new file mode 100644 index 0000000000000..88e832894432b --- /dev/null +++ b/drivers/gpu/drm/xe/xe_tile_sriov_vf.c @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: MIT +/* + * Copyright © 2025 Intel Corporation + */ + +#include + +#include "regs/xe_gtt_defs.h" + +#include "xe_assert.h" +#include "xe_ggtt.h" +#include "xe_gt_sriov_vf.h" +#include "xe_sriov.h" +#include "xe_tile_sriov_vf.h" +#include "xe_wopcm.h" + +static int vf_init_ggtt_balloons(struct xe_tile *tile) +{ + struct xe_ggtt *ggtt = tile->mem.ggtt; + + xe_tile_assert(tile, IS_SRIOV_VF(tile_to_xe(tile))); + + tile->sriov.vf.ggtt_balloon[0] = xe_ggtt_node_init(ggtt); + if (IS_ERR(tile->sriov.vf.ggtt_balloon[0])) + return PTR_ERR(tile->sriov.vf.ggtt_balloon[0]); + + tile->sriov.vf.ggtt_balloon[1] = xe_ggtt_node_init(ggtt); + if (IS_ERR(tile->sriov.vf.ggtt_balloon[1])) { + xe_ggtt_node_fini(tile->sriov.vf.ggtt_balloon[0]); + return PTR_ERR(tile->sriov.vf.ggtt_balloon[1]); + } + + return 0; +} + +/** + * xe_tile_sriov_vf_balloon_ggtt_locked - Insert balloon nodes to limit used GGTT address range. + * @tile: the &xe_tile struct instance + * + * Return: 0 on success or a negative error code on failure. + */ +int xe_tile_sriov_vf_balloon_ggtt_locked(struct xe_tile *tile) +{ + u64 ggtt_base = xe_gt_sriov_vf_ggtt_base(tile->primary_gt); + u64 ggtt_size = xe_gt_sriov_vf_ggtt(tile->primary_gt); + struct xe_device *xe = tile_to_xe(tile); + u64 start, end; + int err; + + xe_tile_assert(tile, IS_SRIOV_VF(xe)); + xe_tile_assert(tile, ggtt_size); + lockdep_assert_held(&tile->mem.ggtt->lock); + + /* + * VF can only use part of the GGTT as allocated by the PF: + * + * WOPCM GUC_GGTT_TOP + * |<------------ Total GGTT size ------------------>| + * + * VF GGTT base -->|<- size ->| + * + * +--------------------+----------+-----------------+ + * |////////////////////| block |\\\\\\\\\\\\\\\\\| + * +--------------------+----------+-----------------+ + * + * |<--- balloon[0] --->|<-- VF -->|<-- balloon[1] ->| + */ + + start = xe_wopcm_size(xe); + end = ggtt_base; + if (end != start) { + err = xe_ggtt_node_insert_balloon_locked(tile->sriov.vf.ggtt_balloon[0], + start, end); + if (err) + return err; + } + + start = ggtt_base + ggtt_size; + end = GUC_GGTT_TOP; + if (end != start) { + err = xe_ggtt_node_insert_balloon_locked(tile->sriov.vf.ggtt_balloon[1], + start, end); + if (err) { + xe_ggtt_node_remove_balloon_locked(tile->sriov.vf.ggtt_balloon[0]); + return err; + } + } + + return 0; +} + +static int vf_balloon_ggtt(struct xe_tile *tile) +{ + struct xe_ggtt *ggtt = tile->mem.ggtt; + int err; + + mutex_lock(&ggtt->lock); + err = xe_tile_sriov_vf_balloon_ggtt_locked(tile); + mutex_unlock(&ggtt->lock); + + return err; +} + +/** + * xe_tile_sriov_vf_deballoon_ggtt_locked - Remove balloon nodes. + * @tile: the &xe_tile struct instance + */ +void xe_tile_sriov_vf_deballoon_ggtt_locked(struct xe_tile *tile) +{ + xe_tile_assert(tile, IS_SRIOV_VF(tile_to_xe(tile))); + + xe_ggtt_node_remove_balloon_locked(tile->sriov.vf.ggtt_balloon[1]); + xe_ggtt_node_remove_balloon_locked(tile->sriov.vf.ggtt_balloon[0]); +} + +static void vf_deballoon_ggtt(struct xe_tile *tile) +{ + mutex_lock(&tile->mem.ggtt->lock); + xe_tile_sriov_vf_deballoon_ggtt_locked(tile); + mutex_unlock(&tile->mem.ggtt->lock); +} + +static void vf_fini_ggtt_balloons(struct xe_tile *tile) +{ + xe_tile_assert(tile, IS_SRIOV_VF(tile_to_xe(tile))); + + xe_ggtt_node_fini(tile->sriov.vf.ggtt_balloon[1]); + xe_ggtt_node_fini(tile->sriov.vf.ggtt_balloon[0]); +} + +static void cleanup_ggtt(struct drm_device *drm, void *arg) +{ + struct xe_tile *tile = arg; + + vf_deballoon_ggtt(tile); + vf_fini_ggtt_balloons(tile); +} + +/** + * xe_tile_sriov_vf_prepare_ggtt - Prepare a VF's GGTT configuration. + * @tile: the &xe_tile + * + * This function is for VF use only. + * + * Return: 0 on success or a negative error code on failure. + */ +int xe_tile_sriov_vf_prepare_ggtt(struct xe_tile *tile) +{ + struct xe_device *xe = tile_to_xe(tile); + int err; + + err = vf_init_ggtt_balloons(tile); + if (err) + return err; + + err = vf_balloon_ggtt(tile); + if (err) { + vf_fini_ggtt_balloons(tile); + return err; + } + + return drmm_add_action_or_reset(&xe->drm, cleanup_ggtt, tile); +} + +/** + * DOC: GGTT nodes shifting during VF post-migration recovery + * + * The first fixup applied to the VF KMD structures as part of post-migration + * recovery is shifting nodes within &xe_ggtt instance. The nodes are moved + * from range previously assigned to this VF, into newly provisioned area. + * The changes include balloons, which are resized accordingly. + * + * The balloon nodes are there to eliminate unavailable ranges from use: one + * reserves the GGTT area below the range for current VF, and another one + * reserves area above. + * + * Below is a GGTT layout of example VF, with a certain address range assigned to + * said VF, and inaccessible areas above and below: + * + * 0 4GiB + * |<--------------------------- Total GGTT size ----------------------------->| + * WOPCM GUC_TOP + * |<-------------- Area mappable by xe_ggtt instance ---------------->| + * + * +---+---------------------------------+----------+----------------------+---+ + * |\\\|/////////////////////////////////| VF mem |//////////////////////|\\\| + * +---+---------------------------------+----------+----------------------+---+ + * + * Hardware enforced access rules before migration: + * + * |<------- inaccessible for VF ------->||<-- inaccessible for VF ->| + * + * GGTT nodes used for tracking allocations: + * + * |<---------- balloon ------------>|<- nodes->|<----- balloon ------>| + * + * After the migration, GGTT area assigned to the VF might have shifted, either + * to lower or to higher address. But we expect the total size and extra areas to + * be identical, as migration can only happen between matching platforms. + * Below is an example of GGTT layout of the VF after migration. Content of the + * GGTT for VF has been moved to a new area, and we receive its address from GuC: + * + * +---+----------------------+----------+---------------------------------+---+ + * |\\\|//////////////////////| VF mem |/////////////////////////////////|\\\| + * +---+----------------------+----------+---------------------------------+---+ + * + * Hardware enforced access rules after migration: + * + * |<- inaccessible for VF -->||<------- inaccessible for VF ------->| + * + * So the VF has a new slice of GGTT assigned, and during migration process, the + * memory content was copied to that new area. But the &xe_ggtt nodes are still + * tracking allocations using the old addresses. The nodes within VF owned area + * have to be shifted, and balloon nodes need to be resized to properly mask out + * areas not owned by the VF. + * + * Fixed &xe_ggtt nodes used for tracking allocations: + * + * |<------ balloon ------>|<- nodes->|<----------- balloon ----------->| + * + * Due to use of GPU profiles, we do not expect the old and new GGTT ares to + * overlap; but our node shifting will fix addresses properly regardless. + */ + +/** + * xe_tile_sriov_vf_fixup_ggtt_nodes - Shift GGTT allocations to match assigned range. + * @tile: the &xe_tile struct instance + * @shift: the shift value + * + * Since Global GTT is not virtualized, each VF has an assigned range + * within the global space. This range might have changed during migration, + * which requires all memory addresses pointing to GGTT to be shifted. + */ +void xe_tile_sriov_vf_fixup_ggtt_nodes(struct xe_tile *tile, s64 shift) +{ + struct xe_ggtt *ggtt = tile->mem.ggtt; + + mutex_lock(&ggtt->lock); + + xe_tile_sriov_vf_deballoon_ggtt_locked(tile); + xe_ggtt_shift_nodes_locked(ggtt, shift); + xe_tile_sriov_vf_balloon_ggtt_locked(tile); + + mutex_unlock(&ggtt->lock); +} diff --git a/drivers/gpu/drm/xe/xe_tile_sriov_vf.h b/drivers/gpu/drm/xe/xe_tile_sriov_vf.h new file mode 100644 index 0000000000000..93eb043171e83 --- /dev/null +++ b/drivers/gpu/drm/xe/xe_tile_sriov_vf.h @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: MIT */ +/* + * Copyright © 2025 Intel Corporation + */ + +#ifndef _XE_TILE_SRIOV_VF_H_ +#define _XE_TILE_SRIOV_VF_H_ + +#include + +struct xe_tile; + +int xe_tile_sriov_vf_prepare_ggtt(struct xe_tile *tile); +int xe_tile_sriov_vf_balloon_ggtt_locked(struct xe_tile *tile); +void xe_tile_sriov_vf_deballoon_ggtt_locked(struct xe_tile *tile); +void xe_tile_sriov_vf_fixup_ggtt_nodes(struct xe_tile *tile, s64 shift); + +#endif